diff --git a/README.md b/README.md index bb6f3ff..8ebcffc 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ The SDK also comes equipped with `HeliusFactory`, a factory for creating instanc ### Embedded Solana Client The `Helius` client has an embedded [Solana client](https://docs.rs/solana-client/latest/solana_client/rpc_client/struct.RpcClient.html) that can be accessed via `helius.connection().request_name()` where `request_name()` is a given [RPC method](https://docs.rs/solana-client/latest/solana_client/rpc_client/struct.RpcClient.html#implementations). A full list of all Solana RPC HTTP methods can be found [here](https://solana.com/docs/rpc/http). +Note that this Solana client is synchronous by default. An asynchronous client can be created using the `new_with_async_solana` in place of the `new` method. The asynchronous client can be accessed via `helius.async_connection()?.some_async_method().await?` where `some_async_method()` is a given async RPC method. + ### Examples More examples of how to use the SDK can be found in the [`examples`](https://github.com/helius-labs/helius-rust-sdk/tree/dev/examples) directory. diff --git a/examples/get_latest_blockhash_async.rs b/examples/get_latest_blockhash_async.rs new file mode 100644 index 0000000..524d291 --- /dev/null +++ b/examples/get_latest_blockhash_async.rs @@ -0,0 +1,18 @@ +use helius::error::Result; +use helius::types::*; +use helius::Helius; + +use solana_sdk::hash::Hash; + +#[tokio::main] +async fn main() -> Result<()> { + let api_key: &str = "your_api_key"; + let cluster: Cluster = Cluster::MainnetBeta; + + let helius: Helius = Helius::new_with_async_solana(api_key, cluster).expect("Failed to create a Helius client"); + + let latest_blockhash: Hash = helius.async_connection()?.get_latest_blockhash().await?; + println!("{:?}", latest_blockhash); + + Ok(()) +} diff --git a/src/client.rs b/src/client.rs index d7dc0e7..898851d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,12 @@ -use std::sync::Arc; +use std::{ops::Deref, sync::Arc}; use crate::config::Config; -use crate::error::Result; +use crate::error::{HeliusError, Result}; use crate::rpc_client::RpcClient; use crate::types::Cluster; use reqwest::Client; +use solana_client::nonblocking::rpc_client::RpcClient as AsyncSolanaRpcClient; use solana_client::rpc_client::RpcClient as SolanaRpcClient; /// The `Helius` struct is the main entry point to interacting with the SDK @@ -19,13 +20,15 @@ pub struct Helius { pub client: Client, /// A reference-counted RPC client tailored for making requests in a thread-safe manner pub rpc_client: Arc, + /// An optional asynchronous Solana client for async operations + pub async_rpc_client: Option>, } impl Helius { /// Creates a new instance of `Helius` configured with a specific API key and a target cluster /// /// # Arguments - /// * `api_key` - The API key required for authenticating requests made + /// * `api_key` - The API key required for authenticating the requests made /// * `cluster` - The Solana cluster (Devnet or MainnetBeta) that defines the given network environment /// /// # Returns @@ -47,6 +50,37 @@ impl Helius { config, client, rpc_client, + async_rpc_client: None, + }) + } + + /// Creates a new instance of `Helius` with an asynchronous Solana client + /// + /// # Arguments + /// * `api_key` - The API key required for authenticating the requests made + /// * `cluster` - The Solana cluster (Devnet or MainnetBeta) that defines the given network environment + /// + /// # Returns + /// An instance of `Helius` if successful. A `HeliusError` is returned if an error occurs during configuration or initialization of the HTTP or RPC client + /// + /// # Example + /// ```rust + /// use helius::Helius; + /// use helius::types::Cluster; + /// + /// let helius = Helius::new_with_async_solana("your_api_key", Cluster::Devnet).expect("Failed to create a Helius client"); + /// ``` + pub fn new_with_async_solana(api_key: &str, cluster: Cluster) -> Result { + let config: Arc = Arc::new(Config::new(api_key, cluster)?); + let client: Client = Client::new(); + let url: String = format!("{}/?api-key={}", config.endpoints.rpc, config.api_key); + let async_solana_client: Arc = Arc::new(AsyncSolanaRpcClient::new(url)); + + Ok(Helius { + config: config.clone(), + client: client.clone(), + rpc_client: Arc::new(RpcClient::new(Arc::new(client), config.clone())?), + async_rpc_client: Some(async_solana_client), }) } @@ -58,7 +92,52 @@ impl Helius { self.rpc_client.clone() } + /// Provides a thread-safe way to access asynchronous Solana client functionalities + /// + /// # Returns + /// A `Result` containing a `HeliusAsyncSolanaClient` if an `async_rpc_client` exists, otherwise a `HeliusError` + pub fn async_connection(&self) -> Result { + match &self.async_rpc_client { + Some(client) => Ok(HeliusAsyncSolanaClient::new(client.clone())), + None => Err(HeliusError::ClientNotInitialized { + text: "An asynchronous Solana RPC client must be initialized before trying to access async_connection" + .to_string(), + }), + } + } + + /// Provides a thread-safe way to access synchronous Solana client functionalities + /// + /// # Returns + /// A cloned `Arc` that can be safely shared across threads pub fn connection(&self) -> Arc { self.rpc_client.solana_client.clone() } } + +/// A wrapper around the asynchronous Solana RPC client that provides thread-safe access +pub struct HeliusAsyncSolanaClient { + client: Arc, +} + +impl HeliusAsyncSolanaClient { + /// Creates a new instance of `HeliusAsyncSolanaClient` + /// + /// # Arguments + /// * `client` - The asynchronous Solana RPC client to wrap + /// + /// # Returns + /// An instance of `HeliusAsyncSolanaClient` + pub fn new(client: Arc) -> Self { + Self { client } + } +} + +impl Deref for HeliusAsyncSolanaClient { + type Target = AsyncSolanaRpcClient; + + /// Dereferences the wrapper to provide access to the underlying asynchronous Solana RPC client + fn deref(&self) -> &Self::Target { + &self.client + } +} diff --git a/src/error.rs b/src/error.rs index 0b65d37..fcde2fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,6 +24,12 @@ pub enum HeliusError { #[error("Solana client error: {0}")] ClientError(#[from] ClientError), + /// Indicates if a client is not already initialized + /// + /// Useful for the new_with_async_solana method on the `Helius` client + #[error("Client not initialized: {text}")] + ClientNotInitialized { text: String }, + /// Represents compile errors from the Solana SDK /// /// This captures all compile errors thrown by the Solana SDK diff --git a/src/factory.rs b/src/factory.rs index 76dab18..41e5e36 100644 --- a/src/factory.rs +++ b/src/factory.rs @@ -89,6 +89,7 @@ impl HeliusFactory { config, client, rpc_client, + async_rpc_client: None, }) } } diff --git a/tests/rpc/test_get_asset.rs b/tests/rpc/test_get_asset.rs index 8ac82d5..d5b84a3 100644 --- a/tests/rpc/test_get_asset.rs +++ b/tests/rpc/test_get_asset.rs @@ -173,6 +173,7 @@ async fn test_get_asset_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAsset = GetAsset { @@ -228,6 +229,7 @@ async fn test_get_asset_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAsset = GetAsset { diff --git a/tests/rpc/test_get_asset_batch.rs b/tests/rpc/test_get_asset_batch.rs index 98296e4..4b4b41d 100644 --- a/tests/rpc/test_get_asset_batch.rs +++ b/tests/rpc/test_get_asset_batch.rs @@ -266,6 +266,7 @@ async fn test_get_asset_batch_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetBatch = GetAssetBatch { @@ -313,6 +314,7 @@ async fn test_get_asset_batch_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetBatch = GetAssetBatch { diff --git a/tests/rpc/test_get_asset_proof.rs b/tests/rpc/test_get_asset_proof.rs index 14e11c3..82965f3 100644 --- a/tests/rpc/test_get_asset_proof.rs +++ b/tests/rpc/test_get_asset_proof.rs @@ -63,6 +63,7 @@ async fn test_get_asset_proof_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetProof = GetAssetProof { @@ -110,6 +111,7 @@ async fn test_get_asset_proof_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetProof = GetAssetProof { diff --git a/tests/rpc/test_get_asset_proof_batch.rs b/tests/rpc/test_get_asset_proof_batch.rs index 8e0e1c4..e4cbd35 100644 --- a/tests/rpc/test_get_asset_proof_batch.rs +++ b/tests/rpc/test_get_asset_proof_batch.rs @@ -66,6 +66,7 @@ async fn test_get_asset_proof_batch_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetProofBatch = GetAssetProofBatch { @@ -121,6 +122,7 @@ async fn test_get_asset_proof_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetProofBatch = GetAssetProofBatch { diff --git a/tests/rpc/test_get_assets_by_authority.rs b/tests/rpc/test_get_assets_by_authority.rs index 881c493..b8db279 100644 --- a/tests/rpc/test_get_assets_by_authority.rs +++ b/tests/rpc/test_get_assets_by_authority.rs @@ -230,6 +230,7 @@ async fn test_get_assets_by_authority_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetsByAuthority = GetAssetsByAuthority { @@ -275,6 +276,7 @@ async fn test_get_assets_by_authority_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetsByAuthority = GetAssetsByAuthority { diff --git a/tests/rpc/test_get_assets_by_creator.rs b/tests/rpc/test_get_assets_by_creator.rs index 2d46427..7d107d6 100644 --- a/tests/rpc/test_get_assets_by_creator.rs +++ b/tests/rpc/test_get_assets_by_creator.rs @@ -230,6 +230,7 @@ async fn test_get_assets_by_creator_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetsByCreator = GetAssetsByCreator { @@ -275,6 +276,7 @@ async fn test_get_assets_by_creator_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetsByCreator = GetAssetsByCreator { diff --git a/tests/rpc/test_get_assets_by_group.rs b/tests/rpc/test_get_assets_by_group.rs index 255baed..164167a 100644 --- a/tests/rpc/test_get_assets_by_group.rs +++ b/tests/rpc/test_get_assets_by_group.rs @@ -145,6 +145,7 @@ async fn test_get_assets_by_group_success() { config, client, rpc_client, + async_rpc_client: None, }; let sorting: AssetSorting = AssetSorting { @@ -203,6 +204,7 @@ async fn test_get_assets_by_group_failure() { config, client, rpc_client, + async_rpc_client: None, }; let sorting: AssetSorting = AssetSorting { diff --git a/tests/rpc/test_get_assets_by_owner.rs b/tests/rpc/test_get_assets_by_owner.rs index f5c5987..0e5a685 100644 --- a/tests/rpc/test_get_assets_by_owner.rs +++ b/tests/rpc/test_get_assets_by_owner.rs @@ -160,6 +160,7 @@ async fn test_get_assets_by_owner_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetsByOwner = GetAssetsByOwner { @@ -209,6 +210,7 @@ async fn test_get_assets_by_owner_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetsByOwner = GetAssetsByOwner { diff --git a/tests/rpc/test_get_nft_editions.rs b/tests/rpc/test_get_nft_editions.rs index b31fa17..65a0ca8 100644 --- a/tests/rpc/test_get_nft_editions.rs +++ b/tests/rpc/test_get_nft_editions.rs @@ -54,9 +54,10 @@ async fn test_get_nft_editions_success() { config, client, rpc_client, + async_rpc_client: None, }; - let request = GetNftEditions { + let request: GetNftEditions = GetNftEditions { mint: Some("Ey2Qb8kLctbchQsMnhZs5DjY32To2QtPuXNwWvk4NosL".to_string()), page: Some(1), limit: Some(1), @@ -105,6 +106,7 @@ async fn test_get_nft_editions_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request = GetNftEditions { diff --git a/tests/rpc/test_get_priority_fee_estimate.rs b/tests/rpc/test_get_priority_fee_estimate.rs index 1654cdb..271b57b 100644 --- a/tests/rpc/test_get_priority_fee_estimate.rs +++ b/tests/rpc/test_get_priority_fee_estimate.rs @@ -52,6 +52,7 @@ async fn test_get_nft_editions_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest { @@ -107,6 +108,7 @@ async fn test_get_nft_editions_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest { diff --git a/tests/rpc/test_get_rwa_asset.rs b/tests/rpc/test_get_rwa_asset.rs index ef4994e..5daecef 100644 --- a/tests/rpc/test_get_rwa_asset.rs +++ b/tests/rpc/test_get_rwa_asset.rs @@ -61,6 +61,7 @@ async fn test_get_rwa_asset_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetRwaAssetRequest = GetRwaAssetRequest { @@ -102,6 +103,7 @@ async fn test_get_rwa_asset_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetRwaAssetRequest = GetRwaAssetRequest { diff --git a/tests/rpc/test_get_signatures_for_asset.rs b/tests/rpc/test_get_signatures_for_asset.rs index ce48927..c308cfa 100644 --- a/tests/rpc/test_get_signatures_for_asset.rs +++ b/tests/rpc/test_get_signatures_for_asset.rs @@ -52,6 +52,7 @@ async fn test_get_asset_signatures_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetSignatures = GetAssetSignatures { @@ -100,6 +101,7 @@ async fn test_get_asset_signatures_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetAssetSignatures = GetAssetSignatures { diff --git a/tests/rpc/test_get_token_accounts.rs b/tests/rpc/test_get_token_accounts.rs index a5f446c..46299f0 100644 --- a/tests/rpc/test_get_token_accounts.rs +++ b/tests/rpc/test_get_token_accounts.rs @@ -59,6 +59,7 @@ async fn test_get_token_accounts_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: GetTokenAccounts = GetTokenAccounts { @@ -110,6 +111,7 @@ async fn test_get_token_accounts_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request = GetTokenAccounts { diff --git a/tests/rpc/test_search_assets.rs b/tests/rpc/test_search_assets.rs index a9e69e2..9be5645 100644 --- a/tests/rpc/test_search_assets.rs +++ b/tests/rpc/test_search_assets.rs @@ -160,6 +160,7 @@ async fn test_search_assets_success() { config, client, rpc_client, + async_rpc_client: None, }; let request: SearchAssets = SearchAssets { @@ -206,6 +207,7 @@ async fn test_search_assets_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: SearchAssets = SearchAssets { diff --git a/tests/test_client.rs b/tests/test_client.rs index ade901c..bc8b2e9 100644 --- a/tests/test_client.rs +++ b/tests/test_client.rs @@ -13,3 +13,16 @@ fn test_creating_new_client_success() { let helius: Helius = result.unwrap(); assert_eq!(helius.config.api_key, api_key); } + +#[test] +fn test_creating_new_async_client_success() { + let api_key: &str = "valid-api-key"; + let cluster: Cluster = Cluster::Devnet; + + let result: Result = Helius::new_with_async_solana(api_key, cluster); + assert!(result.is_ok()); + + let helius: Helius = result.unwrap(); + assert_eq!(helius.config.api_key, api_key); + assert!(helius.async_rpc_client.is_some()); +} diff --git a/tests/test_enhanced_transactions.rs b/tests/test_enhanced_transactions.rs index dc95b72..0881854 100644 --- a/tests/test_enhanced_transactions.rs +++ b/tests/test_enhanced_transactions.rs @@ -79,6 +79,7 @@ async fn test_parse_transactions_success() { config, client, rpc_client, + async_rpc_client: None, }; let request = ParseTransactionsRequest { @@ -117,6 +118,7 @@ async fn test_parse_transactions_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request = ParseTransactionsRequest { transactions: vec![ @@ -205,6 +207,7 @@ async fn test_parse_transaction_history_success() { config, client, rpc_client, + async_rpc_client: None, }; let request = ParsedTransactionHistoryRequest { @@ -242,6 +245,7 @@ async fn test_parse_transaction_history_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request = ParsedTransactionHistoryRequest { address: "46tC8n6GyWvUjFxpTE9juG5WZ72RXADpPhY4S1d6wvTi".to_string(), diff --git a/tests/test_mint_api.rs b/tests/test_mint_api.rs index 5d49499..a174f62 100644 --- a/tests/test_mint_api.rs +++ b/tests/test_mint_api.rs @@ -48,6 +48,7 @@ async fn test_mint_compressed_nft() { config, client, rpc_client, + async_rpc_client: None, }; let request: MintCompressedNftRequest = MintCompressedNftRequest { @@ -121,6 +122,7 @@ async fn test_get_asset_proof_failure() { config, client, rpc_client, + async_rpc_client: None, }; let request: MintCompressedNftRequest = MintCompressedNftRequest { diff --git a/tests/webhook/test_append_addresses_to_webhook.rs b/tests/webhook/test_append_addresses_to_webhook.rs index 737f611..ebb8d9d 100644 --- a/tests/webhook/test_append_addresses_to_webhook.rs +++ b/tests/webhook/test_append_addresses_to_webhook.rs @@ -63,6 +63,7 @@ async fn test_append_addresses_to_webhook_success() { config, client, rpc_client, + async_rpc_client: None, }; let response = helius @@ -129,6 +130,7 @@ async fn test_append_addresses_to_webhook_failure() { config, client, rpc_client, + async_rpc_client: None, }; let response: Result = helius .append_addresses_to_webhook( diff --git a/tests/webhook/test_create_webhook.rs b/tests/webhook/test_create_webhook.rs index d259a6e..77593ea 100644 --- a/tests/webhook/test_create_webhook.rs +++ b/tests/webhook/test_create_webhook.rs @@ -46,6 +46,7 @@ async fn test_create_webhook_success() { config, client, rpc_client, + async_rpc_client: None, }; let request = CreateWebhookRequest { @@ -101,6 +102,7 @@ async fn test_create_webhook_failure() { config, client, rpc_client, + async_rpc_client: None, }; let response: Result = helius.create_webhook(request).await; assert!(response.is_err(), "Expected an error due to server failure"); diff --git a/tests/webhook/test_delete_webhook.rs b/tests/webhook/test_delete_webhook.rs index 588f3a0..daf1290 100644 --- a/tests/webhook/test_delete_webhook.rs +++ b/tests/webhook/test_delete_webhook.rs @@ -32,6 +32,7 @@ async fn test_delete_webhook_success() { config, client, rpc_client, + async_rpc_client: None, }; let response = helius.delete_webhook("0e8250a1-ceec-4757-ad69").await; @@ -65,6 +66,7 @@ async fn test_delete_webhook_failure() { config, client, rpc_client, + async_rpc_client: None, }; let response = helius.delete_webhook("0e8250a1-ceec-4757-ad69").await; assert!(response.is_err(), "Expected an error due to server failure"); diff --git a/tests/webhook/test_edit_webhook.rs b/tests/webhook/test_edit_webhook.rs index 857a72e..7243ae4 100644 --- a/tests/webhook/test_edit_webhook.rs +++ b/tests/webhook/test_edit_webhook.rs @@ -46,6 +46,7 @@ async fn test_edit_webhook_success() { config, client, rpc_client, + async_rpc_client: None, }; let request = EditWebhookRequest { @@ -109,6 +110,7 @@ async fn test_edit_webhook_failure() { config, client, rpc_client, + async_rpc_client: None, }; let response: Result = helius.edit_webhook(request).await; assert!(response.is_err(), "Expected an error due to server failure"); diff --git a/tests/webhook/test_get_all_webhooks.rs b/tests/webhook/test_get_all_webhooks.rs index c488f32..ea49e24 100644 --- a/tests/webhook/test_get_all_webhooks.rs +++ b/tests/webhook/test_get_all_webhooks.rs @@ -44,6 +44,7 @@ async fn test_get_all_webhooks_success() { config, client, rpc_client, + async_rpc_client: None, }; let response = helius.get_all_webhooks().await; @@ -85,6 +86,7 @@ async fn test_get_all_webhooks_failure() { config, client, rpc_client, + async_rpc_client: None, }; let response: Result, HeliusError> = helius.get_all_webhooks().await; assert!(response.is_err(), "Expected an error due to server failure"); diff --git a/tests/webhook/test_get_webhook_by_id.rs b/tests/webhook/test_get_webhook_by_id.rs index e988b9b..eb10317 100644 --- a/tests/webhook/test_get_webhook_by_id.rs +++ b/tests/webhook/test_get_webhook_by_id.rs @@ -45,6 +45,7 @@ async fn test_get_webhook_by_id_success() { config, client, rpc_client, + async_rpc_client: None, }; let response = helius.get_webhook_by_id("0e8250a1-ceec-4757-ad69").await; @@ -85,6 +86,7 @@ async fn test_get_webhook_by_id_failure() { config, client, rpc_client, + async_rpc_client: None, }; let response: Result = helius.get_webhook_by_id("0e8250a1-ceec-4757-ad69").await; assert!(response.is_err(), "Expected an error due to server failure"); diff --git a/tests/webhook/test_remove_addresses_from_webhook.rs b/tests/webhook/test_remove_addresses_from_webhook.rs index 3022e81..9958aa1 100644 --- a/tests/webhook/test_remove_addresses_from_webhook.rs +++ b/tests/webhook/test_remove_addresses_from_webhook.rs @@ -67,6 +67,7 @@ async fn test_remove_addresses_from_webhook_success() { config, client, rpc_client, + async_rpc_client: None, }; let response = helius @@ -140,6 +141,7 @@ async fn test_remove_addresses_from_webhook_failure() { config, client, rpc_client, + async_rpc_client: None, }; let response: Result = helius .remove_addresses_from_webhook(