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