How to use Solana Rust API and building a Rust Library Crate

image

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 provides PubKey struct.
  • solana-client provides RpcClient 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 implement Cluster enum with a method endpoint() 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.

Thank you!