Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): Add an Async Solana Client #37

Merged
merged 10 commits into from
May 30, 2024
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
18 changes: 18 additions & 0 deletions examples/get_latest_blockhash_async.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
85 changes: 82 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<RpcClient>,
/// An optional asynchronous Solana client for async operations
pub async_rpc_client: Option<Arc<AsyncSolanaRpcClient>>,
}

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
Expand All @@ -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<Self> {
let config: Arc<Config> = 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<AsyncSolanaRpcClient> = 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),
})
}

Expand All @@ -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<HeliusAsyncSolanaClient> {
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<SolanaRpcClient>` that can be safely shared across threads
pub fn connection(&self) -> Arc<SolanaRpcClient> {
self.rpc_client.solana_client.clone()
}
}

/// A wrapper around the asynchronous Solana RPC client that provides thread-safe access
pub struct HeliusAsyncSolanaClient {
client: Arc<AsyncSolanaRpcClient>,
}

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<AsyncSolanaRpcClient>) -> 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
}
}
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ impl HeliusFactory {
config,
client,
rpc_client,
async_rpc_client: None,
})
}
}
2 changes: 2 additions & 0 deletions tests/rpc/test_get_asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ async fn test_get_asset_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAsset = GetAsset {
Expand Down Expand Up @@ -228,6 +229,7 @@ async fn test_get_asset_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAsset = GetAsset {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_asset_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ async fn test_get_asset_batch_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetBatch = GetAssetBatch {
Expand Down Expand Up @@ -313,6 +314,7 @@ async fn test_get_asset_batch_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetBatch = GetAssetBatch {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_asset_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ async fn test_get_asset_proof_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetProof = GetAssetProof {
Expand Down Expand Up @@ -110,6 +111,7 @@ async fn test_get_asset_proof_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetProof = GetAssetProof {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_asset_proof_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async fn test_get_asset_proof_batch_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetProofBatch = GetAssetProofBatch {
Expand Down Expand Up @@ -121,6 +122,7 @@ async fn test_get_asset_proof_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetProofBatch = GetAssetProofBatch {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_assets_by_authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ async fn test_get_assets_by_authority_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetsByAuthority = GetAssetsByAuthority {
Expand Down Expand Up @@ -275,6 +276,7 @@ async fn test_get_assets_by_authority_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetsByAuthority = GetAssetsByAuthority {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_assets_by_creator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ async fn test_get_assets_by_creator_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetsByCreator = GetAssetsByCreator {
Expand Down Expand Up @@ -275,6 +276,7 @@ async fn test_get_assets_by_creator_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetsByCreator = GetAssetsByCreator {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_assets_by_group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ async fn test_get_assets_by_group_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let sorting: AssetSorting = AssetSorting {
Expand Down Expand Up @@ -203,6 +204,7 @@ async fn test_get_assets_by_group_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let sorting: AssetSorting = AssetSorting {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_assets_by_owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ async fn test_get_assets_by_owner_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetsByOwner = GetAssetsByOwner {
Expand Down Expand Up @@ -209,6 +210,7 @@ async fn test_get_assets_by_owner_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetsByOwner = GetAssetsByOwner {
Expand Down
4 changes: 3 additions & 1 deletion tests/rpc/test_get_nft_editions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -105,6 +106,7 @@ async fn test_get_nft_editions_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request = GetNftEditions {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_priority_fee_estimate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ async fn test_get_nft_editions_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
Expand Down Expand Up @@ -107,6 +108,7 @@ async fn test_get_nft_editions_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetPriorityFeeEstimateRequest = GetPriorityFeeEstimateRequest {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_rwa_asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ async fn test_get_rwa_asset_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetRwaAssetRequest = GetRwaAssetRequest {
Expand Down Expand Up @@ -102,6 +103,7 @@ async fn test_get_rwa_asset_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetRwaAssetRequest = GetRwaAssetRequest {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_signatures_for_asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ async fn test_get_asset_signatures_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetSignatures = GetAssetSignatures {
Expand Down Expand Up @@ -100,6 +101,7 @@ async fn test_get_asset_signatures_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetAssetSignatures = GetAssetSignatures {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_get_token_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ async fn test_get_token_accounts_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: GetTokenAccounts = GetTokenAccounts {
Expand Down Expand Up @@ -110,6 +111,7 @@ async fn test_get_token_accounts_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request = GetTokenAccounts {
Expand Down
2 changes: 2 additions & 0 deletions tests/rpc/test_search_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ async fn test_search_assets_success() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: SearchAssets = SearchAssets {
Expand Down Expand Up @@ -206,6 +207,7 @@ async fn test_search_assets_failure() {
config,
client,
rpc_client,
async_rpc_client: None,
};

let request: SearchAssets = SearchAssets {
Expand Down
Loading
Loading