In this article, we will explore and learn how to use Solana Rust API by building a Rust library that fetches accoun balance from Solana Clusters (Mainnet/Testnet/Devnet).
Solana is an open-source, high performance, permissionless blockchain and provides convenient APIs to interact with clusters to perform various operations.
In this article, we will explore and learn how to use Solana Rust API by building a Rust library that fetches account balance from Solana Clusters (Mainnet/Testnet/Devnet).
The library source code we write below is available on GitHub and is published on crates.io (
solana-account-balance
).
I also built a web application (Rust server, ReactJs UX) that demonstrates usage of this library inside another rust crate. It is available on GitHub. Build/run instructions are available in repository.
Lets get started!
# Prerequisites
All you need is Rust installed in your system! If it’s not installed, grab it from Rust’s official site. You can verify your installation with following commands:
$ rustc --version
$ cargo --version
# Creating a new Rust package
We will be using cargo
for all our package management tasks. Create a new package with following command:
$ cargo new solana-balance --lib
This will create a new folder named solana-balance
with following structure
$ cd solana-balance
$ tree .
.
├── Cargo.toml
└── src
└── lib.rs
Cargo.toml
is the manifest file that contains all metadata cargo needs to compile the package. We will use this file to tell cargo about external crates our program will need to work with Solana nodes.
# Adding required dependencies
After setting up the package, we need to add Solana Rust API crates to our package. Solana provides a few official crates. We will be using solana-sdk
and solana-client
crates.
solana-sdk
providesPubKey
struct.solana-client
providesRpcClient
used to connect to solana nodes.
Open Cargo.toml
and add the above mentioned dependencies. The file should look something like this (versions might differ):
[package]
name = "solana-balance"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
solana-client = "1.10.8"
solana-sdk = "1.10.8"
Great! We have the required dependencies. Lets jump to code!
# Code - Get account balance
Copy-paste the following code in lib.rs
file. We will the go through the code line by line.
//! A very simple library to fetch Account Balance from Solana Clusters.
use std::str::FromStr;
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
/// Contains Account balance in Lamports and SOL.
#[derive(Debug)]
pub struct SolanaBalance {
pub lamports: u64,
pub sol: f64,
}
/// Contains any error reported.
#[derive(Debug)]
pub struct SolanaError {
pub error: String,
}
/// Available solana clusters.
pub enum Cluster {
/// The Testnet Cluster.
Testnet,
/// The Devnet Cluster.
Devnet,
/// The Mainnet Beta Cluster.
MainnetBeta,
}
impl Cluster {
/// method to get endpoint URL for cluster.
fn endpoint(&self) -> &str {
match self {
&Cluster::Devnet => "https://api.devnet.solana.com",
&Cluster::MainnetBeta => "https://api.mainnet-beta.solana.com",
&Cluster::Testnet => "https://api.testnet.solana.com",
}
}
}
/// Function to get account balance from Solana Cluster
pub fn get_solana_balance(pubkey: &str, cluster: Cluster) -> Result<SolanaBalance, SolanaError> {
let rpc = RpcClient::new(String::from(cluster.endpoint()));
let pubkey = match Pubkey::from_str(pubkey) {
Ok(key) => key,
Err(err) => {
return Err(SolanaError {
error: err.to_string(),
});
}
};
match rpc.get_account(&pubkey) {
Ok(acc) => {
let balance: SolanaBalance = SolanaBalance {
lamports: acc.lamports,
sol: (acc.lamports as f64) / 1000000000.0,
};
Ok(balance)
}
Err(err) => {
return Err(SolanaError {
error: err.to_string(),
});
}
}
}
#[cfg(test)]
mod tests {
use solana_sdk::pubkey::ParsePubkeyError;
use super::*;
const CORRECT_ACC_ADDRESS: &str = "9aavjzd4iAbiJHawgS7kunfCJefSRRVKso61vzAX9Ho5";
const INCORRECT_ACC_ADDRESS: &str = "wrongaddress";
const ACCOUNT_NOT_FOUND: &str = "888vjzd4iAbiJHawgS7kunfCJefSRRVKso61vzAX9111";
#[test]
fn get_balance() {
let result = get_solana_balance(CORRECT_ACC_ADDRESS, Cluster::Devnet).unwrap();
assert_eq!(result.lamports, 599985000);
assert_eq!(result.sol, 0.599985);
}
#[test]
fn invalid_pubkey() {
let result = get_solana_balance(INCORRECT_ACC_ADDRESS, Cluster::Devnet)
.err()
.unwrap();
assert_eq!(result.error, ParsePubkeyError::WrongSize.to_string());
}
#[test]
fn acc_not_found() {
let result = get_solana_balance(ACCOUNT_NOT_FOUND, Cluster::Devnet)
.err()
.unwrap();
assert_eq!(
result.error,
format!("AccountNotFound: pubkey={}", ACCOUNT_NOT_FOUND)
);
}
}
1. use declarations
use std::str::FromStr;
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
We add use declarations to refer to external module items. solana_sdk
, solana_client
are external crates added as dependencies in Cargo.toml
file. std
crate is standard library crate provided by rust. (Rust automatically replaces -
with _
when referring crate name in code.)
2. Declaring necessary structs/enums
Next, we declare the required structs and enums.
/// Contains Account balance in Lamports and SOL.
#[derive(Debug)]
pub struct SolanaBalance {
pub lamports: u64,
pub sol: f64,
}
/// Contains any error reported.
#[derive(Debug)]
pub struct SolanaError {
pub error: String,
}
/// Available solana clusters.
pub enum Cluster {
/// The Testnet Cluster.
Testnet,
/// The Devnet Cluster.
Devnet,
/// The Mainnet Beta Cluster.
MainnetBeta,
}
impl Cluster {
/// method to get endpoint URL for cluster.
fn endpoint(&self) -> &str {
match self {
&Cluster::Devnet => "https://api.devnet.solana.com",
&Cluster::MainnetBeta => "https://api.mainnet-beta.solana.com",
&Cluster::Testnet => "https://api.testnet.solana.com",
}
}
}
SolanaBalance
struct is used to hold balance received from cluster.SolanaError
struct is used to store error messages.Cluster
enum is used to represent possible clusters this library can connect to. Since we will connect to any one of the available clusters, enum is used. Further, we implementCluster
enum with a methodendpoint()
and map each of it’s value with corresponding URL (we will see this in action in a moment).
3. Defining get_solana_balance() function
And here comes the heart of the program. This function takes pubkey
and cluster
as input, connects with the cluster, fetches account info and returns account balance or any error.
/// Function to get account balance from Solana Cluster
pub fn get_solana_balance(pubkey: &str, cluster: Cluster) -> Result<SolanaBalance, SolanaError> {
let rpc = RpcClient::new(String::from(cluster.endpoint()));
let pubkey = match Pubkey::from_str(pubkey) {
Ok(key) => key,
Err(err) => {
return Err(SolanaError {
error: err.to_string(),
});
}
};
match rpc.get_account(&pubkey) {
Ok(acc) => {
let balance: SolanaBalance = SolanaBalance {
lamports: acc.lamports,
sol: (acc.lamports as f64) / 1000000000.0,
};
Ok(balance)
}
Err(err) => {
return Err(SolanaError {
error: err.to_string(),
});
}
}
}
Lets break it down a bit.
The function takes a string pubkey address (pubkey: &str
) and Cluster
(enum) as input and returns a Result<SolanaBalance, SolanaError>
. We are returning a Result<>
because there is a possibility of error (incorrect pubkey, cluster network error, etc.). If we get balance successfully, we get SolanaBalance
. If there is an error, we get SolanaError
.
let rpc = RpcClient::new(String::from(cluster.endpoint()));
let pubkey = match Pubkey::from_str(pubkey) {
Ok(key) => key,
Err(err) => {
return Err(SolanaError {
error: err.to_string(),
});
}
};
On basis of cluster
received, we connect to Solana RPC endpoint. The RpcClient::new()
function takes String
input. We had implemented Cluster
enum with a method endpoint()
that returns a &str
endpoint URL. So for input cluster
we call cluster.endpoint()
to get it’s corresponding URL and use String::from()
to convert &str
to String
.
Next, we need a PubKey
struct instance. We can construct PubKey
instance from &str
using from_str()
associated function. Pubkey
implements FromStr
trait (which is provided by rust standard library) which gives PubKey
access to FromStr
’s from_str()
function. PubKey::from_str()
returns a Result<>
. Which is why we use match
operator to check if the provided pubkey
is valid or not.
match rpc.get_account(&pubkey) {
Ok(acc) => {
let balance: SolanaBalance = SolanaBalance {
lamports: acc.lamports,
sol: (acc.lamports as f64) / 1000000000.0,
};
Ok(balance)
}
Err(err) => {
return Err(SolanaError {
error: err.to_string(),
});
}
}
With PubKey
and RpcClient
instances we can connect to solana cluster. RpcClient
provides a method get_account()
that takes &PubKey
as input and returns ClientResult<Account>
. ClientResult<T>
is nothing but a Result<T, ClientError>
type. If successful, we get Account
. On error, we get ClientError
.
Account
struct has a field lamports
which gives us the account balance in lamports. We read lamports
, convert it to SOL, populate SolanaBalance
and return as Ok()
;
In case of any error, we populate SolanaError
with error message and return as Err()
.
That’s it! This completes our get_account_balance()
function.
# Testing get_account_balance() function
#[cfg(test)]
mod tests {
use solana_sdk::pubkey::ParsePubkeyError;
use super::*;
const CORRECT_ACC_ADDRESS: &str = "9aavjzd4iAbiJHawgS7kunfCJefSRRVKso61vzAX9Ho5";
const INCORRECT_ACC_ADDRESS: &str = "wrongaddress";
const ACCOUNT_NOT_FOUND: &str = "888vjzd4iAbiJHawgS7kunfCJefSRRVKso61vzAX9111";
#[test]
fn get_balance() {
let result = get_solana_balance(CORRECT_ACC_ADDRESS, Cluster::Devnet).unwrap();
assert_eq!(result.lamports, 599985000);
assert_eq!(result.sol, 0.599985);
}
#[test]
fn invalid_pubkey() {
let result = get_solana_balance(INCORRECT_ACC_ADDRESS, Cluster::Devnet)
.err()
.unwrap();
assert_eq!(result.error, ParsePubkeyError::WrongSize.to_string());
}
#[test]
fn acc_not_found() {
let result = get_solana_balance(ACCOUNT_NOT_FOUND, Cluster::Devnet)
.err()
.unwrap();
assert_eq!(
result.error,
format!("AccountNotFound: pubkey={}", ACCOUNT_NOT_FOUND)
);
}
}
To verify if our function works, I wrote a few tests with possible input values.
Tests can be run with following command:
$ cargo test
Expected output:
running 3 tests
test tests::invalid_pubkey ... ok
test tests::acc_not_found ... ok
test tests::get_balance ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.76s
And this completes our library function! We are able to fetch account balance successfully, and are able to identify if some error occured.
Congratulations!
# Using Library
After completing the library and writing tests, the crate can be published to crates.io for other users to use. Detailed instructions for publishing a crate is available in docs.
Once published, other users can add this library as dependency and use it. (We can specify dependencies from git repository in Cargo.toml
if the crate has not been published anywhere. Instructions here)
The above library’s source code is available on GitHub for reference. Feel free to play with it. I am open to contributions.
The library is published on crates.io as well (solana-account-balance
).
I have created a sample web application to demonstrate usage of this library inside another rust application. The web application is available on GitHub. Build/Run instructions are available in repository.