Skip to content

Commit

Permalink
Merge pull request #37 from helius-labs/feat/async-solana-client
Browse files Browse the repository at this point in the history
feat(client): Add an Async Solana Client
  • Loading branch information
0xIchigo authored May 30, 2024
2 parents ed4cb4f + c5c8625 commit c8a2643
Show file tree
Hide file tree
Showing 29 changed files with 171 additions and 4 deletions.
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

0 comments on commit c8a2643

Please sign in to comment.