1use async_trait::async_trait;
12use eyre::Result;
13#[cfg(test)]
14use mockall::automock;
15use mpl_token_metadata::accounts::Metadata;
16use reqwest::Url;
17use serde::Serialize;
18use solana_client::{
19 client_error::{ClientError, ClientErrorKind},
20 nonblocking::rpc_client::RpcClient,
21 rpc_request::RpcRequest,
22 rpc_response::{RpcPrioritizationFee, RpcSimulateTransactionResult},
23};
24use solana_commitment_config::CommitmentConfig;
25use solana_sdk::{
26 account::Account,
27 hash::Hash,
28 message::Message,
29 program_pack::Pack,
30 pubkey::Pubkey,
31 signature::Signature,
32 transaction::{Transaction, VersionedTransaction},
33};
34use spl_token_interface::state::Mint;
35use std::{str::FromStr, sync::Arc, time::Duration};
36use thiserror::Error;
37
38use crate::{
39 models::{RpcConfig, SolanaTransactionStatus},
40 services::provider::{retry_rpc_call, should_mark_provider_failed_by_status_code},
41};
42
43use super::ProviderError;
44use super::{
45 rpc_selector::{RpcSelector, RpcSelectorError},
46 ProviderConfig, RetryConfig,
47};
48
49use crate::utils::validate_safe_url;
50
51fn matches_error_pattern(error_msg: &str, pattern: &str) -> bool {
57 let normalized_msg = error_msg.to_lowercase().replace(' ', "");
58 let normalized_pattern = pattern.to_lowercase().replace(' ', "");
59 normalized_msg.contains(&normalized_pattern)
60}
61
62#[derive(Error, Debug, Serialize)]
66pub enum SolanaProviderError {
67 #[error("Network error: {0}")]
69 NetworkError(String),
70
71 #[error("RPC error: {0}")]
73 RpcError(String),
74
75 #[error("Request error (HTTP {status_code}): {error}")]
77 RequestError { error: String, status_code: u16 },
78
79 #[error("Invalid address: {0}")]
81 InvalidAddress(String),
82
83 #[error("RPC selector error: {0}")]
85 SelectorError(RpcSelectorError),
86
87 #[error("Network configuration error: {0}")]
89 NetworkConfiguration(String),
90
91 #[error("Insufficient funds for transaction: {0}")]
93 InsufficientFunds(String),
94
95 #[error("Blockhash not found or expired: {0}")]
97 BlockhashNotFound(String),
98
99 #[error("Invalid transaction: {0}")]
101 InvalidTransaction(String),
102
103 #[error("Transaction already processed: {0}")]
105 AlreadyProcessed(String),
106}
107
108impl SolanaProviderError {
109 pub fn is_transient(&self) -> bool {
129 match self {
130 SolanaProviderError::NetworkError(_) => true,
132 SolanaProviderError::RpcError(_) => true,
133 SolanaProviderError::BlockhashNotFound(_) => true,
134 SolanaProviderError::SelectorError(_) => true,
135
136 SolanaProviderError::RequestError { status_code, .. } => match *status_code {
138 501 | 505 => false, 500 | 502..=504 | 506..=599 => true,
143
144 408 | 425 | 429 => true,
146
147 400..=499 => false,
149
150 _ => false,
152 },
153
154 SolanaProviderError::InsufficientFunds(_) => false,
156 SolanaProviderError::InvalidTransaction(_) => false,
157 SolanaProviderError::AlreadyProcessed(_) => false,
158 SolanaProviderError::InvalidAddress(_) => false,
159 SolanaProviderError::NetworkConfiguration(_) => false,
160 }
161 }
162
163 pub fn from_rpc_error(error: ClientError) -> Self {
168 match error.kind() {
169 ClientErrorKind::Io(_) => SolanaProviderError::NetworkError(error.to_string()),
171
172 ClientErrorKind::Reqwest(reqwest_err) => {
174 if let Some(status) = reqwest_err.status() {
175 SolanaProviderError::RequestError {
176 error: error.to_string(),
177 status_code: status.as_u16(),
178 }
179 } else {
180 SolanaProviderError::NetworkError(error.to_string())
182 }
183 }
184
185 ClientErrorKind::RpcError(rpc_err) => {
187 let rpc_err_str = format!("{rpc_err}");
188 Self::from_rpc_response_error(&rpc_err_str, &error)
189 }
190
191 ClientErrorKind::TransactionError(tx_error) => {
193 Self::from_transaction_error(tx_error, &error)
194 }
195
196 ClientErrorKind::Custom(msg) => {
198 Self::from_rpc_response_error(msg, &error)
200 }
201
202 _ => SolanaProviderError::RpcError(error.to_string()),
204 }
205 }
206
207 fn from_rpc_response_error(rpc_err: &str, full_error: &ClientError) -> Self {
228 let error_str = rpc_err;
229
230 if error_str.contains("-32002") {
232 if matches_error_pattern(error_str, "blockhash not found") {
234 SolanaProviderError::BlockhashNotFound(full_error.to_string())
235 } else if matches_error_pattern(error_str, "insufficient funds") {
236 SolanaProviderError::InsufficientFunds(full_error.to_string())
237 } else {
238 SolanaProviderError::InvalidTransaction(full_error.to_string())
240 }
241 } else if error_str.contains("-32003") {
242 SolanaProviderError::InvalidTransaction(full_error.to_string())
244 } else if error_str.contains("-32004") {
245 SolanaProviderError::RpcError(full_error.to_string())
247 } else if error_str.contains("-32005") {
248 SolanaProviderError::RpcError(full_error.to_string())
250 } else if error_str.contains("-32007") {
251 SolanaProviderError::NetworkConfiguration(full_error.to_string())
253 } else if error_str.contains("-32008") {
254 SolanaProviderError::BlockhashNotFound(full_error.to_string())
256 } else if error_str.contains("-32009") {
257 SolanaProviderError::AlreadyProcessed(full_error.to_string())
259 } else if error_str.contains("-32010") {
260 SolanaProviderError::NetworkConfiguration(full_error.to_string())
262 } else if error_str.contains("-32013") {
263 SolanaProviderError::InvalidTransaction(full_error.to_string())
265 } else if error_str.contains("-32014") {
266 SolanaProviderError::RpcError(full_error.to_string())
268 } else if error_str.contains("-32015") {
269 SolanaProviderError::InvalidTransaction(full_error.to_string())
271 } else if error_str.contains("-32016") {
272 SolanaProviderError::RpcError(full_error.to_string())
274 } else if error_str.contains("-32602") {
275 SolanaProviderError::InvalidTransaction(full_error.to_string())
277 } else {
278 if matches_error_pattern(error_str, "insufficient funds") {
280 SolanaProviderError::InsufficientFunds(full_error.to_string())
281 } else if matches_error_pattern(error_str, "blockhash not found") {
282 SolanaProviderError::BlockhashNotFound(full_error.to_string())
283 } else if matches_error_pattern(error_str, "already processed") {
284 SolanaProviderError::AlreadyProcessed(full_error.to_string())
285 } else {
286 SolanaProviderError::RpcError(full_error.to_string())
288 }
289 }
290 }
291
292 fn from_transaction_error(
294 tx_error: &solana_sdk::transaction::TransactionError,
295 full_error: &ClientError,
296 ) -> Self {
297 use solana_sdk::transaction::TransactionError as TxErr;
298
299 match tx_error {
300 TxErr::InsufficientFundsForFee | TxErr::InsufficientFundsForRent { .. } => {
302 SolanaProviderError::InsufficientFunds(full_error.to_string())
303 }
304
305 TxErr::BlockhashNotFound => {
307 SolanaProviderError::BlockhashNotFound(full_error.to_string())
308 }
309
310 TxErr::AlreadyProcessed => {
312 SolanaProviderError::AlreadyProcessed(full_error.to_string())
313 }
314
315 TxErr::SignatureFailure
317 | TxErr::MissingSignatureForFee
318 | TxErr::InvalidAccountForFee
319 | TxErr::AccountNotFound
320 | TxErr::InvalidAccountIndex
321 | TxErr::InvalidProgramForExecution
322 | TxErr::ProgramAccountNotFound
323 | TxErr::InstructionError(_, _)
324 | TxErr::CallChainTooDeep
325 | TxErr::InvalidWritableAccount
326 | TxErr::InvalidRentPayingAccount
327 | TxErr::WouldExceedMaxBlockCostLimit
328 | TxErr::WouldExceedMaxAccountCostLimit
329 | TxErr::WouldExceedMaxVoteCostLimit
330 | TxErr::WouldExceedAccountDataBlockLimit
331 | TxErr::TooManyAccountLocks
332 | TxErr::AddressLookupTableNotFound
333 | TxErr::InvalidAddressLookupTableOwner
334 | TxErr::InvalidAddressLookupTableData
335 | TxErr::InvalidAddressLookupTableIndex
336 | TxErr::MaxLoadedAccountsDataSizeExceeded
337 | TxErr::InvalidLoadedAccountsDataSizeLimit
338 | TxErr::ResanitizationNeeded
339 | TxErr::ProgramExecutionTemporarilyRestricted { .. }
340 | TxErr::AccountBorrowOutstanding => {
341 SolanaProviderError::InvalidTransaction(full_error.to_string())
342 }
343
344 TxErr::AccountInUse | TxErr::AccountLoadedTwice | TxErr::ClusterMaintenance => {
346 SolanaProviderError::RpcError(full_error.to_string())
347 }
348
349 _ => SolanaProviderError::RpcError(full_error.to_string()),
351 }
352 }
353}
354
355#[async_trait]
357#[cfg_attr(test, automock)]
358#[allow(dead_code)]
359pub trait SolanaProviderTrait: Send + Sync {
360 fn get_configs(&self) -> Vec<RpcConfig>;
361 async fn get_balance(&self, address: &str) -> Result<u64, SolanaProviderError>;
363
364 async fn get_latest_blockhash(&self) -> Result<Hash, SolanaProviderError>;
366
367 async fn get_latest_blockhash_with_commitment(
369 &self,
370 commitment: CommitmentConfig,
371 ) -> Result<(Hash, u64), SolanaProviderError>;
372
373 async fn send_transaction(
375 &self,
376 transaction: &Transaction,
377 ) -> Result<Signature, SolanaProviderError>;
378
379 async fn send_versioned_transaction(
381 &self,
382 transaction: &VersionedTransaction,
383 ) -> Result<Signature, SolanaProviderError>;
384
385 async fn confirm_transaction(&self, signature: &Signature)
387 -> Result<bool, SolanaProviderError>;
388
389 async fn get_minimum_balance_for_rent_exemption(
391 &self,
392 data_size: usize,
393 ) -> Result<u64, SolanaProviderError>;
394
395 async fn simulate_transaction(
397 &self,
398 transaction: &Transaction,
399 ) -> Result<RpcSimulateTransactionResult, SolanaProviderError>;
400
401 async fn get_account_from_str(&self, account: &str) -> Result<Account, SolanaProviderError>;
403
404 async fn get_account_from_pubkey(
406 &self,
407 pubkey: &Pubkey,
408 ) -> Result<Account, SolanaProviderError>;
409
410 async fn get_token_metadata_from_pubkey(
412 &self,
413 pubkey: &str,
414 ) -> Result<TokenMetadata, SolanaProviderError>;
415
416 async fn is_blockhash_valid(
418 &self,
419 hash: &Hash,
420 commitment: CommitmentConfig,
421 ) -> Result<bool, SolanaProviderError>;
422
423 async fn get_fee_for_message(&self, message: &Message) -> Result<u64, SolanaProviderError>;
425
426 async fn get_recent_prioritization_fees(
428 &self,
429 addresses: &[Pubkey],
430 ) -> Result<Vec<RpcPrioritizationFee>, SolanaProviderError>;
431
432 async fn calculate_total_fee(&self, message: &Message) -> Result<u64, SolanaProviderError>;
434
435 async fn get_transaction_status(
437 &self,
438 signature: &Signature,
439 ) -> Result<SolanaTransactionStatus, SolanaProviderError>;
440
441 async fn raw_request_dyn(
443 &self,
444 method: &str,
445 params: serde_json::Value,
446 ) -> Result<serde_json::Value, SolanaProviderError>;
447}
448
449#[derive(Debug)]
450pub struct SolanaProvider {
451 selector: RpcSelector,
453 timeout_seconds: Duration,
455 commitment: CommitmentConfig,
457 retry_config: RetryConfig,
459}
460
461impl From<String> for SolanaProviderError {
462 fn from(s: String) -> Self {
463 SolanaProviderError::RpcError(s)
464 }
465}
466
467fn should_mark_solana_provider_failed(error: &SolanaProviderError) -> bool {
474 match error {
475 SolanaProviderError::RequestError { status_code, .. } => {
476 should_mark_provider_failed_by_status_code(*status_code)
477 }
478 _ => false,
479 }
480}
481
482#[derive(Error, Debug, PartialEq)]
483pub struct TokenMetadata {
484 pub decimals: u8,
485 pub symbol: String,
486 pub mint: String,
487}
488
489impl std::fmt::Display for TokenMetadata {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 write!(
492 f,
493 "TokenMetadata {{ decimals: {}, symbol: {}, mint: {} }}",
494 self.decimals, self.symbol, self.mint
495 )
496 }
497}
498
499#[allow(dead_code)]
500impl SolanaProvider {
501 pub fn new(config: ProviderConfig) -> Result<Self, ProviderError> {
502 Self::new_with_commitment_and_health(
503 config.rpc_configs,
504 config.timeout_seconds,
505 CommitmentConfig::confirmed(),
506 config.failure_threshold,
507 config.pause_duration_secs,
508 config.failure_expiration_secs,
509 )
510 }
511
512 pub fn new_with_commitment_and_health(
527 configs: Vec<RpcConfig>,
528 timeout_seconds: u64,
529 commitment: CommitmentConfig,
530 failure_threshold: u32,
531 pause_duration_secs: u64,
532 failure_expiration_secs: u64,
533 ) -> Result<Self, ProviderError> {
534 if configs.is_empty() {
535 return Err(ProviderError::NetworkConfiguration(
536 "At least one RPC configuration must be provided".to_string(),
537 ));
538 }
539
540 RpcConfig::validate_list(&configs)
541 .map_err(|e| ProviderError::NetworkConfiguration(format!("Invalid URL: {e}")))?;
542
543 let selector = RpcSelector::new(
545 configs,
546 failure_threshold,
547 pause_duration_secs,
548 failure_expiration_secs,
549 )
550 .map_err(|e| {
551 ProviderError::NetworkConfiguration(format!("Failed to create RPC selector: {e}"))
552 })?;
553
554 let retry_config = RetryConfig::from_env();
555
556 Ok(Self {
557 selector,
558 timeout_seconds: Duration::from_secs(timeout_seconds),
559 commitment,
560 retry_config,
561 })
562 }
563
564 pub fn get_configs(&self) -> Vec<RpcConfig> {
569 self.selector.get_configs()
570 }
571
572 fn get_client(&self) -> Result<RpcClient, SolanaProviderError> {
581 self.selector
582 .get_client(
583 |url| {
584 Ok(RpcClient::new_with_timeout_and_commitment(
585 url.to_string(),
586 self.timeout_seconds,
587 self.commitment,
588 ))
589 },
590 &std::collections::HashSet::new(),
591 )
592 .map_err(SolanaProviderError::SelectorError)
593 }
594
595 fn initialize_provider(&self, url: &str) -> Result<Arc<RpcClient>, SolanaProviderError> {
605 let allowed_hosts = crate::config::ServerConfig::get_rpc_allowed_hosts();
607 let block_private_ips = crate::config::ServerConfig::get_rpc_block_private_ips();
608 validate_safe_url(url, &allowed_hosts, block_private_ips).map_err(|e| {
609 SolanaProviderError::NetworkConfiguration(format!(
610 "RPC URL security validation failed: {e}"
611 ))
612 })?;
613
614 let rpc_url: Url = url.parse().map_err(|e| {
615 SolanaProviderError::NetworkConfiguration(format!("Invalid URL format: {e}"))
616 })?;
617
618 let client = RpcClient::new_with_timeout_and_commitment(
619 rpc_url.to_string(),
620 self.timeout_seconds,
621 self.commitment,
622 );
623
624 Ok(Arc::new(client))
625 }
626
627 async fn retry_rpc_call<T, F, Fut>(
629 &self,
630 operation_name: &str,
631 operation: F,
632 ) -> Result<T, SolanaProviderError>
633 where
634 F: Fn(Arc<RpcClient>) -> Fut,
635 Fut: std::future::Future<Output = Result<T, SolanaProviderError>>,
636 {
637 let is_retriable = |e: &SolanaProviderError| e.is_transient();
638
639 tracing::debug!(
640 "Starting RPC operation '{}' with timeout: {}s",
641 operation_name,
642 self.timeout_seconds.as_secs()
643 );
644
645 retry_rpc_call(
646 &self.selector,
647 operation_name,
648 is_retriable,
649 should_mark_solana_provider_failed,
650 |url| match self.initialize_provider(url) {
651 Ok(provider) => Ok(provider),
652 Err(e) => Err(e),
653 },
654 operation,
655 Some(self.retry_config.clone()),
656 )
657 .await
658 }
659}
660
661#[async_trait]
662#[allow(dead_code)]
663impl SolanaProviderTrait for SolanaProvider {
664 fn get_configs(&self) -> Vec<RpcConfig> {
665 self.get_configs()
666 }
667
668 async fn get_balance(&self, address: &str) -> Result<u64, SolanaProviderError> {
674 let pubkey = Pubkey::from_str(address)
675 .map_err(|e| SolanaProviderError::InvalidAddress(e.to_string()))?;
676
677 self.retry_rpc_call("get_balance", |client| async move {
678 client
679 .get_balance(&pubkey)
680 .await
681 .map_err(SolanaProviderError::from_rpc_error)
682 })
683 .await
684 }
685
686 async fn is_blockhash_valid(
688 &self,
689 hash: &Hash,
690 commitment: CommitmentConfig,
691 ) -> Result<bool, SolanaProviderError> {
692 self.retry_rpc_call("is_blockhash_valid", |client| async move {
693 client
694 .is_blockhash_valid(hash, commitment)
695 .await
696 .map_err(SolanaProviderError::from_rpc_error)
697 })
698 .await
699 }
700
701 async fn get_latest_blockhash(&self) -> Result<Hash, SolanaProviderError> {
703 self.retry_rpc_call("get_latest_blockhash", |client| async move {
704 client
705 .get_latest_blockhash()
706 .await
707 .map_err(SolanaProviderError::from_rpc_error)
708 })
709 .await
710 }
711
712 async fn get_latest_blockhash_with_commitment(
713 &self,
714 commitment: CommitmentConfig,
715 ) -> Result<(Hash, u64), SolanaProviderError> {
716 self.retry_rpc_call(
717 "get_latest_blockhash_with_commitment",
718 |client| async move {
719 client
720 .get_latest_blockhash_with_commitment(commitment)
721 .await
722 .map_err(SolanaProviderError::from_rpc_error)
723 },
724 )
725 .await
726 }
727
728 async fn send_transaction(
730 &self,
731 transaction: &Transaction,
732 ) -> Result<Signature, SolanaProviderError> {
733 self.retry_rpc_call("send_transaction", |client| async move {
734 client
735 .send_transaction(transaction)
736 .await
737 .map_err(SolanaProviderError::from_rpc_error)
738 })
739 .await
740 }
741
742 async fn send_versioned_transaction(
744 &self,
745 transaction: &VersionedTransaction,
746 ) -> Result<Signature, SolanaProviderError> {
747 self.retry_rpc_call("send_transaction", |client| async move {
748 client
749 .send_transaction(transaction)
750 .await
751 .map_err(SolanaProviderError::from_rpc_error)
752 })
753 .await
754 }
755
756 async fn confirm_transaction(
758 &self,
759 signature: &Signature,
760 ) -> Result<bool, SolanaProviderError> {
761 self.retry_rpc_call("confirm_transaction", |client| async move {
762 client
763 .confirm_transaction(signature)
764 .await
765 .map_err(SolanaProviderError::from_rpc_error)
766 })
767 .await
768 }
769
770 async fn get_minimum_balance_for_rent_exemption(
772 &self,
773 data_size: usize,
774 ) -> Result<u64, SolanaProviderError> {
775 self.retry_rpc_call(
776 "get_minimum_balance_for_rent_exemption",
777 |client| async move {
778 client
779 .get_minimum_balance_for_rent_exemption(data_size)
780 .await
781 .map_err(SolanaProviderError::from_rpc_error)
782 },
783 )
784 .await
785 }
786
787 async fn simulate_transaction(
789 &self,
790 transaction: &Transaction,
791 ) -> Result<RpcSimulateTransactionResult, SolanaProviderError> {
792 self.retry_rpc_call("simulate_transaction", |client| async move {
793 client
794 .simulate_transaction(transaction)
795 .await
796 .map_err(SolanaProviderError::from_rpc_error)
797 .map(|response| response.value)
798 })
799 .await
800 }
801
802 async fn get_account_from_str(&self, account: &str) -> Result<Account, SolanaProviderError> {
804 let address = Pubkey::from_str(account).map_err(|e| {
805 SolanaProviderError::InvalidAddress(format!("Invalid pubkey {account}: {e}"))
806 })?;
807 self.retry_rpc_call("get_account", |client| async move {
808 client
809 .get_account(&address)
810 .await
811 .map_err(SolanaProviderError::from_rpc_error)
812 })
813 .await
814 }
815
816 async fn get_account_from_pubkey(
818 &self,
819 pubkey: &Pubkey,
820 ) -> Result<Account, SolanaProviderError> {
821 self.retry_rpc_call("get_account_from_pubkey", |client| async move {
822 client
823 .get_account(pubkey)
824 .await
825 .map_err(SolanaProviderError::from_rpc_error)
826 })
827 .await
828 }
829
830 async fn get_token_metadata_from_pubkey(
832 &self,
833 pubkey: &str,
834 ) -> Result<TokenMetadata, SolanaProviderError> {
835 let mint_pubkey = Pubkey::from_str(pubkey).map_err(|e| {
837 SolanaProviderError::InvalidAddress(format!("Invalid pubkey {pubkey}: {e}"))
838 })?;
839
840 let account = self.get_account_from_pubkey(&mint_pubkey).await?;
842
843 let decimals = Mint::unpack(&account.data)
845 .map_err(|e| {
846 SolanaProviderError::InvalidTransaction(format!(
847 "Failed to unpack mint info for {pubkey}: {e}"
848 ))
849 })?
850 .decimals;
851
852 let mint_pubkey_program =
855 solana_program::pubkey::Pubkey::new_from_array(mint_pubkey.to_bytes());
856 let metadata_pda_program = Metadata::find_pda(&mint_pubkey_program).0;
857
858 let metadata_pda = Pubkey::new_from_array(metadata_pda_program.to_bytes());
860
861 let symbol = match self.get_account_from_pubkey(&metadata_pda).await {
862 Ok(metadata_account) => match Metadata::from_bytes(&metadata_account.data) {
863 Ok(metadata) => metadata.symbol.trim_end_matches('\u{0}').to_string(),
864 Err(_) => String::new(),
865 },
866 Err(_) => String::new(), };
868
869 Ok(TokenMetadata {
870 decimals,
871 symbol,
872 mint: pubkey.to_string(),
873 })
874 }
875
876 async fn get_fee_for_message(&self, message: &Message) -> Result<u64, SolanaProviderError> {
878 self.retry_rpc_call("get_fee_for_message", |client| async move {
879 client
880 .get_fee_for_message(message)
881 .await
882 .map_err(SolanaProviderError::from_rpc_error)
883 })
884 .await
885 }
886
887 async fn get_recent_prioritization_fees(
888 &self,
889 addresses: &[Pubkey],
890 ) -> Result<Vec<RpcPrioritizationFee>, SolanaProviderError> {
891 self.retry_rpc_call("get_recent_prioritization_fees", |client| async move {
892 client
893 .get_recent_prioritization_fees(addresses)
894 .await
895 .map_err(SolanaProviderError::from_rpc_error)
896 })
897 .await
898 }
899
900 async fn calculate_total_fee(&self, message: &Message) -> Result<u64, SolanaProviderError> {
901 let base_fee = self.get_fee_for_message(message).await?;
902 let priority_fees = self.get_recent_prioritization_fees(&[]).await?;
903
904 let max_priority_fee = priority_fees
905 .iter()
906 .map(|fee| fee.prioritization_fee)
907 .max()
908 .unwrap_or(0);
909
910 Ok(base_fee + max_priority_fee)
911 }
912
913 async fn get_transaction_status(
914 &self,
915 signature: &Signature,
916 ) -> Result<SolanaTransactionStatus, SolanaProviderError> {
917 let result = self
918 .retry_rpc_call("get_transaction_status", |client| async move {
919 client
920 .get_signature_statuses_with_history(&[*signature])
921 .await
922 .map_err(SolanaProviderError::from_rpc_error)
923 })
924 .await?;
925
926 let status = result.value.first();
927
928 match status {
929 Some(Some(v)) => {
930 if v.err.is_some() {
931 Ok(SolanaTransactionStatus::Failed)
932 } else if v.satisfies_commitment(CommitmentConfig::finalized()) {
933 Ok(SolanaTransactionStatus::Finalized)
934 } else if v.satisfies_commitment(CommitmentConfig::confirmed()) {
935 Ok(SolanaTransactionStatus::Confirmed)
936 } else {
937 Ok(SolanaTransactionStatus::Processed)
938 }
939 }
940 Some(None) => Err(SolanaProviderError::RpcError(
941 "Transaction confirmation status not available".to_string(),
942 )),
943 None => Err(SolanaProviderError::RpcError(
944 "Transaction confirmation status not available".to_string(),
945 )),
946 }
947 }
948
949 async fn raw_request_dyn(
951 &self,
952 method: &str,
953 params: serde_json::Value,
954 ) -> Result<serde_json::Value, SolanaProviderError> {
955 let params_owned = params.clone();
956 let method_static: &'static str = Box::leak(method.to_string().into_boxed_str());
957 self.retry_rpc_call("raw_request_dyn", move |client| {
958 let params_for_call = params_owned.clone();
959 async move {
960 client
961 .send(
962 RpcRequest::Custom {
963 method: method_static,
964 },
965 params_for_call,
966 )
967 .await
968 .map_err(|e| SolanaProviderError::RpcError(e.to_string()))
969 }
970 })
971 .await
972 }
973}
974
975#[cfg(test)]
976mod tests {
977 use super::*;
978 use lazy_static::lazy_static;
979 use solana_sdk::{
980 hash::Hash,
981 message::Message,
982 signer::{keypair::Keypair, Signer},
983 transaction::Transaction,
984 };
985 use std::sync::Mutex;
986
987 lazy_static! {
988 static ref EVM_TEST_ENV_MUTEX: Mutex<()> = Mutex::new(());
989 }
990
991 struct EvmTestEnvGuard {
992 _mutex_guard: std::sync::MutexGuard<'static, ()>,
993 }
994
995 impl EvmTestEnvGuard {
996 fn new(mutex_guard: std::sync::MutexGuard<'static, ()>) -> Self {
997 std::env::set_var(
998 "API_KEY",
999 "test_api_key_for_evm_provider_new_this_is_long_enough_32_chars",
1000 );
1001 std::env::set_var("REDIS_URL", "redis://test-dummy-url-for-evm-provider");
1002
1003 Self {
1004 _mutex_guard: mutex_guard,
1005 }
1006 }
1007 }
1008
1009 impl Drop for EvmTestEnvGuard {
1010 fn drop(&mut self) {
1011 std::env::remove_var("API_KEY");
1012 std::env::remove_var("REDIS_URL");
1013 }
1014 }
1015
1016 fn setup_test_env() -> EvmTestEnvGuard {
1018 let guard = EVM_TEST_ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
1019 EvmTestEnvGuard::new(guard)
1020 }
1021
1022 fn get_funded_keypair() -> Keypair {
1023 Keypair::try_from(
1025 [
1026 120, 248, 160, 20, 225, 60, 226, 195, 68, 137, 176, 87, 21, 129, 0, 76, 144, 129,
1027 122, 250, 80, 4, 247, 50, 248, 82, 146, 77, 139, 156, 40, 41, 240, 161, 15, 81,
1028 198, 198, 86, 167, 90, 148, 131, 13, 184, 222, 251, 71, 229, 212, 169, 2, 72, 202,
1029 150, 184, 176, 148, 75, 160, 255, 233, 73, 31,
1030 ]
1031 .as_slice(),
1032 )
1033 .unwrap()
1034 }
1035
1036 async fn get_recent_blockhash(provider: &SolanaProvider) -> Hash {
1038 provider
1039 .get_latest_blockhash()
1040 .await
1041 .expect("Failed to get blockhash")
1042 }
1043
1044 fn create_test_rpc_config() -> RpcConfig {
1045 RpcConfig {
1046 url: "https://api.devnet.solana.com".to_string(),
1047 weight: 1,
1048 ..Default::default()
1049 }
1050 }
1051
1052 fn create_test_provider_config(configs: Vec<RpcConfig>, timeout: u64) -> ProviderConfig {
1053 ProviderConfig::new(configs, timeout, 3, 60, 60)
1054 }
1055
1056 #[tokio::test]
1057 async fn test_new_with_valid_config() {
1058 let _env_guard = setup_test_env();
1059 let configs = vec![create_test_rpc_config()];
1060 let timeout = 30;
1061
1062 let result = SolanaProvider::new(create_test_provider_config(configs, timeout));
1063
1064 assert!(result.is_ok());
1065 let provider = result.unwrap();
1066 assert_eq!(provider.timeout_seconds, Duration::from_secs(timeout));
1067 assert_eq!(provider.commitment, CommitmentConfig::confirmed());
1068 }
1069
1070 #[tokio::test]
1071 async fn test_new_with_commitment_valid_config() {
1072 let _env_guard = setup_test_env();
1073
1074 let configs = vec![create_test_rpc_config()];
1075 let timeout = 30;
1076 let commitment = CommitmentConfig::finalized();
1077
1078 let result =
1079 SolanaProvider::new_with_commitment_and_health(configs, timeout, commitment, 3, 60, 60);
1080
1081 assert!(result.is_ok());
1082 let provider = result.unwrap();
1083 assert_eq!(provider.timeout_seconds, Duration::from_secs(timeout));
1084 assert_eq!(provider.commitment, commitment);
1085 }
1086
1087 #[tokio::test]
1088 async fn test_new_with_empty_configs() {
1089 let _env_guard = setup_test_env();
1090 let configs: Vec<RpcConfig> = vec![];
1091 let timeout = 30;
1092
1093 let result = SolanaProvider::new(create_test_provider_config(configs, timeout));
1094
1095 assert!(result.is_err());
1096 assert!(matches!(
1097 result,
1098 Err(ProviderError::NetworkConfiguration(_))
1099 ));
1100 }
1101
1102 #[tokio::test]
1103 async fn test_new_with_commitment_empty_configs() {
1104 let _env_guard = setup_test_env();
1105 let configs: Vec<RpcConfig> = vec![];
1106 let timeout = 30;
1107 let commitment = CommitmentConfig::finalized();
1108
1109 let result =
1110 SolanaProvider::new_with_commitment_and_health(configs, timeout, commitment, 3, 60, 60);
1111
1112 assert!(result.is_err());
1113 assert!(matches!(
1114 result,
1115 Err(ProviderError::NetworkConfiguration(_))
1116 ));
1117 }
1118
1119 #[tokio::test]
1120 async fn test_new_with_invalid_url() {
1121 let _env_guard = setup_test_env();
1122 let configs = vec![RpcConfig {
1123 url: "invalid-url".to_string(),
1124 weight: 1,
1125 ..Default::default()
1126 }];
1127 let timeout = 30;
1128
1129 let result = SolanaProvider::new(create_test_provider_config(configs, timeout));
1130
1131 assert!(result.is_err());
1132 assert!(matches!(
1133 result,
1134 Err(ProviderError::NetworkConfiguration(_))
1135 ));
1136 }
1137
1138 #[tokio::test]
1139 async fn test_new_with_commitment_invalid_url() {
1140 let _env_guard = setup_test_env();
1141 let configs = vec![RpcConfig {
1142 url: "invalid-url".to_string(),
1143 weight: 1,
1144 ..Default::default()
1145 }];
1146 let timeout = 30;
1147 let commitment = CommitmentConfig::finalized();
1148
1149 let result =
1150 SolanaProvider::new_with_commitment_and_health(configs, timeout, commitment, 3, 60, 60);
1151
1152 assert!(result.is_err());
1153 assert!(matches!(
1154 result,
1155 Err(ProviderError::NetworkConfiguration(_))
1156 ));
1157 }
1158
1159 #[tokio::test]
1160 async fn test_new_with_multiple_configs() {
1161 let _env_guard = setup_test_env();
1162 let configs = vec![
1163 create_test_rpc_config(),
1164 RpcConfig {
1165 url: "https://api.mainnet-beta.solana.com".to_string(),
1166 weight: 1,
1167 ..Default::default()
1168 },
1169 ];
1170 let timeout = 30;
1171
1172 let result = SolanaProvider::new(create_test_provider_config(configs, timeout));
1173
1174 assert!(result.is_ok());
1175 }
1176
1177 #[tokio::test]
1178 async fn test_provider_creation() {
1179 let _env_guard = setup_test_env();
1180 let configs = vec![create_test_rpc_config()];
1181 let timeout = 30;
1182 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout));
1183 assert!(provider.is_ok());
1184 }
1185
1186 #[tokio::test]
1187 async fn test_get_balance() {
1188 let _env_guard = setup_test_env();
1189 let configs = vec![create_test_rpc_config()];
1190 let timeout = 30;
1191 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1192 let keypair = Keypair::new();
1193 let balance = provider.get_balance(&keypair.pubkey().to_string()).await;
1194 assert!(balance.is_ok());
1195 assert_eq!(balance.unwrap(), 0);
1196 }
1197
1198 #[tokio::test]
1199 async fn test_get_balance_funded_account() {
1200 let _env_guard = setup_test_env();
1201 let configs = vec![create_test_rpc_config()];
1202 let timeout = 30;
1203 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1204 let keypair = get_funded_keypair();
1205 let balance = provider.get_balance(&keypair.pubkey().to_string()).await;
1206 assert!(balance.is_ok());
1207 assert_eq!(balance.unwrap(), 1000000000);
1208 }
1209
1210 #[tokio::test]
1211 async fn test_get_latest_blockhash() {
1212 let _env_guard = setup_test_env();
1213 let configs = vec![create_test_rpc_config()];
1214 let timeout = 30;
1215 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1216 let blockhash = provider.get_latest_blockhash().await;
1217 assert!(blockhash.is_ok());
1218 }
1219
1220 #[tokio::test]
1221 async fn test_simulate_transaction() {
1222 let _env_guard = setup_test_env();
1223 let configs = vec![create_test_rpc_config()];
1224 let timeout = 30;
1225 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout))
1226 .expect("Failed to create provider");
1227
1228 let fee_payer = get_funded_keypair();
1229
1230 let message = Message::new(&[], Some(&fee_payer.pubkey()));
1233
1234 let mut tx = Transaction::new_unsigned(message);
1235
1236 let recent_blockhash = get_recent_blockhash(&provider).await;
1237 tx.try_sign(&[&fee_payer], recent_blockhash)
1238 .expect("Failed to sign transaction");
1239
1240 let simulation_result = provider.simulate_transaction(&tx).await;
1241
1242 assert!(
1243 simulation_result.is_ok(),
1244 "Simulation failed: {:?}",
1245 simulation_result
1246 );
1247
1248 let result = simulation_result.unwrap();
1249 assert!(
1252 result.err.is_none(),
1253 "Simulation encountered an error: {:?}",
1254 result.err
1255 );
1256 }
1257
1258 #[tokio::test]
1259 async fn test_get_token_metadata_from_pubkey() {
1260 let _env_guard = setup_test_env();
1261 let configs = vec![RpcConfig {
1262 url: "https://api.mainnet-beta.solana.com".to_string(),
1263 weight: 1,
1264 ..Default::default()
1265 }];
1266 let timeout = 30;
1267 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1268 let usdc_token_metadata = provider
1269 .get_token_metadata_from_pubkey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
1270 .await
1271 .unwrap();
1272
1273 assert_eq!(
1274 usdc_token_metadata,
1275 TokenMetadata {
1276 decimals: 6,
1277 symbol: "USDC".to_string(),
1278 mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
1279 }
1280 );
1281
1282 let usdt_token_metadata = provider
1283 .get_token_metadata_from_pubkey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")
1284 .await
1285 .unwrap();
1286
1287 assert_eq!(
1288 usdt_token_metadata,
1289 TokenMetadata {
1290 decimals: 6,
1291 symbol: "USDT".to_string(),
1292 mint: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB".to_string(),
1293 }
1294 );
1295 }
1296
1297 #[tokio::test]
1298 async fn test_get_client_success() {
1299 let _env_guard = setup_test_env();
1300 let configs = vec![create_test_rpc_config()];
1301 let timeout = 30;
1302 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1303
1304 let client = provider.get_client();
1305 assert!(client.is_ok());
1306
1307 let client = client.unwrap();
1308 let health_result = client.get_health().await;
1309 assert!(health_result.is_ok());
1310 }
1311
1312 #[tokio::test]
1313 async fn test_get_client_with_custom_commitment() {
1314 let _env_guard = setup_test_env();
1315 let configs = vec![create_test_rpc_config()];
1316 let timeout = 30;
1317 let commitment = CommitmentConfig::finalized();
1318
1319 let provider =
1320 SolanaProvider::new_with_commitment_and_health(configs, timeout, commitment, 3, 60, 60)
1321 .unwrap();
1322
1323 let client = provider.get_client();
1324 assert!(client.is_ok());
1325
1326 let client = client.unwrap();
1327 let health_result = client.get_health().await;
1328 assert!(health_result.is_ok());
1329 }
1330
1331 #[tokio::test]
1332 async fn test_get_client_with_multiple_rpcs() {
1333 let _env_guard = setup_test_env();
1334 let configs = vec![
1335 create_test_rpc_config(),
1336 RpcConfig {
1337 url: "https://api.mainnet-beta.solana.com".to_string(),
1338 weight: 2,
1339 ..Default::default()
1340 },
1341 ];
1342 let timeout = 30;
1343
1344 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1345
1346 let client_result = provider.get_client();
1347 assert!(client_result.is_ok());
1348
1349 for _ in 0..5 {
1351 let client = provider.get_client();
1352 assert!(client.is_ok());
1353 }
1354 }
1355
1356 #[test]
1357 fn test_initialize_provider_valid_url() {
1358 let _env_guard = setup_test_env();
1359
1360 let configs = vec![RpcConfig {
1361 url: "https://api.devnet.solana.com".to_string(),
1362 weight: 1,
1363 ..Default::default()
1364 }];
1365 let provider = SolanaProvider::new(create_test_provider_config(configs, 10)).unwrap();
1366 let result = provider.initialize_provider("https://api.devnet.solana.com");
1367 assert!(result.is_ok());
1368 let arc_client = result.unwrap();
1369 let _client: &RpcClient = Arc::as_ref(&arc_client);
1371 }
1372
1373 #[test]
1374 fn test_initialize_provider_invalid_url() {
1375 let _env_guard = setup_test_env();
1376
1377 let configs = vec![RpcConfig {
1378 url: "https://api.devnet.solana.com".to_string(),
1379 weight: 1,
1380 ..Default::default()
1381 }];
1382 let provider = SolanaProvider::new(create_test_provider_config(configs, 10)).unwrap();
1383 let result = provider.initialize_provider("not-a-valid-url");
1384 assert!(result.is_err());
1385 match result {
1386 Err(SolanaProviderError::NetworkConfiguration(msg)) => {
1387 assert!(msg.contains("Invalid URL format"))
1388 }
1389 _ => panic!("Expected NetworkConfiguration error"),
1390 }
1391 }
1392
1393 #[test]
1394 fn test_from_string_for_solana_provider_error() {
1395 let msg = "some rpc error".to_string();
1396 let err: SolanaProviderError = msg.clone().into();
1397 match err {
1398 SolanaProviderError::RpcError(inner) => assert_eq!(inner, msg),
1399 _ => panic!("Expected RpcError variant"),
1400 }
1401 }
1402
1403 #[test]
1404 fn test_matches_error_pattern() {
1405 assert!(matches_error_pattern(
1407 "blockhash not found",
1408 "blockhash not found"
1409 ));
1410 assert!(matches_error_pattern(
1411 "insufficient funds",
1412 "insufficient funds"
1413 ));
1414
1415 assert!(matches_error_pattern(
1417 "BLOCKHASH NOT FOUND",
1418 "blockhash not found"
1419 ));
1420 assert!(matches_error_pattern(
1421 "blockhash not found",
1422 "BLOCKHASH NOT FOUND"
1423 ));
1424 assert!(matches_error_pattern(
1425 "BlockHash Not Found",
1426 "blockhash not found"
1427 ));
1428
1429 assert!(matches_error_pattern(
1431 "blockhashnotfound",
1432 "blockhash not found"
1433 ));
1434 assert!(matches_error_pattern(
1435 "blockhash not found",
1436 "blockhashnotfound"
1437 ));
1438 assert!(matches_error_pattern(
1439 "insufficientfunds",
1440 "insufficient funds"
1441 ));
1442
1443 assert!(matches_error_pattern(
1445 "BLOCKHASHNOTFOUND",
1446 "blockhash not found"
1447 ));
1448 assert!(matches_error_pattern(
1449 "blockhash not found",
1450 "BLOCKHASHNOTFOUND"
1451 ));
1452 assert!(matches_error_pattern(
1453 "BlockHashNotFound",
1454 "blockhash not found"
1455 ));
1456 assert!(matches_error_pattern(
1457 "INSUFFICIENTFUNDS",
1458 "insufficient funds"
1459 ));
1460
1461 assert!(matches_error_pattern(
1463 "transaction failed: blockhash not found",
1464 "blockhash not found"
1465 ));
1466 assert!(matches_error_pattern(
1467 "error: insufficient funds for transaction",
1468 "insufficient funds"
1469 ));
1470 assert!(matches_error_pattern(
1471 "BLOCKHASHNOTFOUND in simulation",
1472 "blockhash not found"
1473 ));
1474
1475 assert!(matches_error_pattern(
1477 "blockhash not found",
1478 "blockhash not found"
1479 ));
1480 assert!(matches_error_pattern(
1481 "insufficient funds",
1482 "insufficient funds"
1483 ));
1484
1485 assert!(!matches_error_pattern(
1487 "account not found",
1488 "blockhash not found"
1489 ));
1490 assert!(!matches_error_pattern(
1491 "invalid signature",
1492 "insufficient funds"
1493 ));
1494 assert!(!matches_error_pattern(
1495 "timeout error",
1496 "blockhash not found"
1497 ));
1498
1499 assert!(matches_error_pattern("", ""));
1501 assert!(matches_error_pattern("blockhash not found", "")); assert!(!matches_error_pattern("", "blockhash not found"));
1503
1504 assert!(matches_error_pattern(
1506 "error code -32008: blockhash not found",
1507 "-32008"
1508 ));
1509 assert!(matches_error_pattern("slot 123456 skipped", "slot"));
1510 assert!(matches_error_pattern("RPC_ERROR_503", "rpc_error_503"));
1511 }
1512
1513 #[test]
1514 fn test_solana_provider_error_is_transient() {
1515 assert!(SolanaProviderError::NetworkError("connection timeout".to_string()).is_transient());
1517 assert!(SolanaProviderError::RpcError("node is behind".to_string()).is_transient());
1518 assert!(
1519 SolanaProviderError::BlockhashNotFound("blockhash expired".to_string()).is_transient()
1520 );
1521 assert!(
1522 SolanaProviderError::SelectorError(RpcSelectorError::AllProvidersFailed).is_transient()
1523 );
1524
1525 assert!(
1527 !SolanaProviderError::InsufficientFunds("not enough balance".to_string())
1528 .is_transient()
1529 );
1530 assert!(
1531 !SolanaProviderError::InvalidTransaction("invalid signature".to_string())
1532 .is_transient()
1533 );
1534 assert!(
1535 !SolanaProviderError::AlreadyProcessed("duplicate transaction".to_string())
1536 .is_transient()
1537 );
1538 assert!(
1539 !SolanaProviderError::InvalidAddress("invalid pubkey format".to_string())
1540 .is_transient()
1541 );
1542 assert!(
1543 !SolanaProviderError::NetworkConfiguration("unsupported operation".to_string())
1544 .is_transient()
1545 );
1546 }
1547
1548 #[tokio::test]
1549 async fn test_get_minimum_balance_for_rent_exemption() {
1550 let _env_guard = super::tests::setup_test_env();
1551 let configs = vec![super::tests::create_test_rpc_config()];
1552 let timeout = 30;
1553 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1554
1555 let result = provider.get_minimum_balance_for_rent_exemption(0).await;
1557 assert!(result.is_ok());
1558 }
1559
1560 #[tokio::test]
1561 async fn test_is_blockhash_valid_for_recent_blockhash() {
1562 let _env_guard = super::tests::setup_test_env();
1563 let configs = vec![super::tests::create_test_rpc_config()];
1564 let timeout = 30;
1565 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1566
1567 let blockhash = provider.get_latest_blockhash().await.unwrap();
1569 let is_valid = provider
1570 .is_blockhash_valid(&blockhash, CommitmentConfig::confirmed())
1571 .await;
1572 assert!(is_valid.is_ok());
1573 }
1574
1575 #[tokio::test]
1576 async fn test_is_blockhash_valid_for_invalid_blockhash() {
1577 let _env_guard = super::tests::setup_test_env();
1578 let configs = vec![super::tests::create_test_rpc_config()];
1579 let timeout = 30;
1580 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1581
1582 let invalid_blockhash = solana_sdk::hash::Hash::new_from_array([0u8; 32]);
1583 let is_valid = provider
1584 .is_blockhash_valid(&invalid_blockhash, CommitmentConfig::confirmed())
1585 .await;
1586 assert!(is_valid.is_ok());
1587 }
1588
1589 #[tokio::test]
1590 async fn test_get_latest_blockhash_with_commitment() {
1591 let _env_guard = super::tests::setup_test_env();
1592 let configs = vec![super::tests::create_test_rpc_config()];
1593 let timeout = 30;
1594 let provider = SolanaProvider::new(create_test_provider_config(configs, timeout)).unwrap();
1595
1596 let commitment = CommitmentConfig::confirmed();
1597 let result = provider
1598 .get_latest_blockhash_with_commitment(commitment)
1599 .await;
1600 assert!(result.is_ok());
1601 let (blockhash, last_valid_block_height) = result.unwrap();
1602 assert_ne!(blockhash, solana_sdk::hash::Hash::new_from_array([0u8; 32]));
1604 assert!(last_valid_block_height > 0);
1605 }
1606
1607 #[test]
1608 fn test_from_rpc_response_error_transaction_simulation_failed() {
1609 let mock_error = create_mock_client_error();
1611
1612 let error_str =
1614 r#"{"code": -32002, "message": "Transaction simulation failed: Blockhash not found"}"#;
1615 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1616 assert!(matches!(result, SolanaProviderError::BlockhashNotFound(_)));
1617
1618 let error_str =
1620 r#"{"code": -32002, "message": "Transaction simulation failed: Insufficient funds"}"#;
1621 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1622 assert!(matches!(result, SolanaProviderError::InsufficientFunds(_)));
1623
1624 let error_str = r#"{"code": -32002, "message": "Transaction simulation failed: Invalid instruction data"}"#;
1626 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1627 assert!(matches!(result, SolanaProviderError::InvalidTransaction(_)));
1628 }
1629
1630 #[test]
1631 fn test_from_rpc_response_error_signature_verification() {
1632 let mock_error = create_mock_client_error();
1633
1634 let error_str = r#"{"code": -32003, "message": "Signature verification failure"}"#;
1636 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1637 assert!(matches!(result, SolanaProviderError::InvalidTransaction(_)));
1638 }
1639
1640 #[test]
1641 fn test_from_rpc_response_error_transient_errors() {
1642 let mock_error = create_mock_client_error();
1643
1644 let error_str = r#"{"code": -32004, "message": "Block not available for slot"}"#;
1646 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1647 assert!(matches!(result, SolanaProviderError::RpcError(_)));
1648
1649 let error_str = r#"{"code": -32005, "message": "Node is behind"}"#;
1651 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1652 assert!(matches!(result, SolanaProviderError::RpcError(_)));
1653
1654 let error_str = r#"{"code": -32008, "message": "Blockhash not found"}"#;
1656 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1657 assert!(matches!(result, SolanaProviderError::BlockhashNotFound(_)));
1658
1659 let error_str = r#"{"code": -32014, "message": "Block status not yet available"}"#;
1661 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1662 assert!(matches!(result, SolanaProviderError::RpcError(_)));
1663
1664 let error_str = r#"{"code": -32016, "message": "Minimum context slot not reached"}"#;
1666 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1667 assert!(matches!(result, SolanaProviderError::RpcError(_)));
1668 }
1669
1670 #[test]
1671 fn test_from_rpc_response_error_permanent_errors() {
1672 let mock_error = create_mock_client_error();
1673
1674 let error_str = r#"{"code": -32007, "message": "Slot skipped"}"#;
1676 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1677 assert!(matches!(
1678 result,
1679 SolanaProviderError::NetworkConfiguration(_)
1680 ));
1681
1682 let error_str = r#"{"code": -32009, "message": "Already processed"}"#;
1684 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1685 assert!(matches!(result, SolanaProviderError::AlreadyProcessed(_)));
1686
1687 let error_str = r#"{"code": -32010, "message": "Key excluded from secondary indexes"}"#;
1689 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1690 assert!(matches!(
1691 result,
1692 SolanaProviderError::NetworkConfiguration(_)
1693 ));
1694
1695 let error_str = r#"{"code": -32013, "message": "Transaction signature length mismatch"}"#;
1697 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1698 assert!(matches!(result, SolanaProviderError::InvalidTransaction(_)));
1699
1700 let error_str = r#"{"code": -32015, "message": "Transaction version not supported"}"#;
1702 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1703 assert!(matches!(result, SolanaProviderError::InvalidTransaction(_)));
1704
1705 let error_str = r#"{"code": -32602, "message": "Invalid params"}"#;
1707 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1708 assert!(matches!(result, SolanaProviderError::InvalidTransaction(_)));
1709 }
1710
1711 #[test]
1712 fn test_from_rpc_response_error_string_pattern_matching() {
1713 let mock_error = create_mock_client_error();
1714
1715 let error_str = r#"{"code": -32000, "message": "INSUFFICIENTFUNDS for transaction"}"#;
1717 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1718 assert!(matches!(result, SolanaProviderError::InsufficientFunds(_)));
1719
1720 let error_str = r#"{"code": -32000, "message": "BlockhashNotFound"}"#;
1721 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1722 assert!(matches!(result, SolanaProviderError::BlockhashNotFound(_)));
1723
1724 let error_str = r#"{"code": -32000, "message": "AlreadyProcessed"}"#;
1725 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1726 assert!(matches!(result, SolanaProviderError::AlreadyProcessed(_)));
1727 }
1728
1729 #[test]
1730 fn test_from_rpc_response_error_unknown_code() {
1731 let mock_error = create_mock_client_error();
1732
1733 let error_str = r#"{"code": -99999, "message": "Unknown error"}"#;
1735 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1736 assert!(matches!(result, SolanaProviderError::RpcError(_)));
1737 }
1738
1739 fn create_mock_client_error() -> ClientError {
1741 use solana_client::rpc_request::RpcRequest;
1742 ClientError::new_with_request(
1744 ClientErrorKind::RpcError(solana_client::rpc_request::RpcError::RpcRequestError(
1745 "test".to_string(),
1746 )),
1747 RpcRequest::GetHealth,
1748 )
1749 }
1750
1751 #[test]
1752 fn test_from_rpc_error_integration() {
1753 let mock_error = create_mock_client_error();
1755
1756 let error_str = r#"{"code": -32000, "message": "Account has insufficient funds"}"#;
1758 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1759 assert!(matches!(result, SolanaProviderError::InsufficientFunds(_)));
1760
1761 let error_str = r#"{"code": -32000, "message": "Blockhash not found"}"#;
1763 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1764 assert!(matches!(result, SolanaProviderError::BlockhashNotFound(_)));
1765
1766 let error_str = r#"{"code": -32000, "message": "Transaction was already processed"}"#;
1768 let result = SolanaProviderError::from_rpc_response_error(error_str, &mock_error);
1769 assert!(matches!(result, SolanaProviderError::AlreadyProcessed(_)));
1770 }
1771
1772 #[test]
1773 fn test_request_error_is_transient() {
1774 let error = SolanaProviderError::RequestError {
1776 error: "Server error".to_string(),
1777 status_code: 500,
1778 };
1779 assert!(error.is_transient());
1780
1781 let error = SolanaProviderError::RequestError {
1782 error: "Bad gateway".to_string(),
1783 status_code: 502,
1784 };
1785 assert!(error.is_transient());
1786
1787 let error = SolanaProviderError::RequestError {
1788 error: "Service unavailable".to_string(),
1789 status_code: 503,
1790 };
1791 assert!(error.is_transient());
1792
1793 let error = SolanaProviderError::RequestError {
1794 error: "Gateway timeout".to_string(),
1795 status_code: 504,
1796 };
1797 assert!(error.is_transient());
1798
1799 let error = SolanaProviderError::RequestError {
1801 error: "Request timeout".to_string(),
1802 status_code: 408,
1803 };
1804 assert!(error.is_transient());
1805
1806 let error = SolanaProviderError::RequestError {
1807 error: "Too early".to_string(),
1808 status_code: 425,
1809 };
1810 assert!(error.is_transient());
1811
1812 let error = SolanaProviderError::RequestError {
1813 error: "Too many requests".to_string(),
1814 status_code: 429,
1815 };
1816 assert!(error.is_transient());
1817
1818 let error = SolanaProviderError::RequestError {
1820 error: "Not implemented".to_string(),
1821 status_code: 501,
1822 };
1823 assert!(!error.is_transient());
1824
1825 let error = SolanaProviderError::RequestError {
1826 error: "HTTP version not supported".to_string(),
1827 status_code: 505,
1828 };
1829 assert!(!error.is_transient());
1830
1831 let error = SolanaProviderError::RequestError {
1833 error: "Bad request".to_string(),
1834 status_code: 400,
1835 };
1836 assert!(!error.is_transient());
1837
1838 let error = SolanaProviderError::RequestError {
1839 error: "Unauthorized".to_string(),
1840 status_code: 401,
1841 };
1842 assert!(!error.is_transient());
1843
1844 let error = SolanaProviderError::RequestError {
1845 error: "Forbidden".to_string(),
1846 status_code: 403,
1847 };
1848 assert!(!error.is_transient());
1849
1850 let error = SolanaProviderError::RequestError {
1851 error: "Not found".to_string(),
1852 status_code: 404,
1853 };
1854 assert!(!error.is_transient());
1855 }
1856
1857 #[test]
1858 fn test_request_error_display() {
1859 let error = SolanaProviderError::RequestError {
1860 error: "Server error".to_string(),
1861 status_code: 500,
1862 };
1863 let error_str = format!("{}", error);
1864 assert!(error_str.contains("HTTP 500"));
1865 assert!(error_str.contains("Server error"));
1866 }
1867}