Solana wallet with Rust: get started now

solana and rust

In this tutorial, we will learn the basics of Solana development by programming a software wallet with Rust. What is a cryptocurrency wallet? A cryptocurrency wallet’s primary function is storing keys that allow you to send and receive cryptocurrency, in this case, Solana. This wallet can be a device or software program. For this tutorial, we will write a Solana wallet with Rust that can generate keys and perform actions.

With our wallet application, we will be able to:

  • airdrop Sol to our own wallet (on test/dev networks)
  • get account balance from a wallet
  • perform transactions, for example sending Sol from one of our wallets to another wallet address we also own
  • Creating and minting NFTs

All this using Rust code. To help us easily connect to Solana RCP servers we will use Solana crates like: solana_client and solana_sdk.

Of course, there are a bunch of Solana wallets out there, but for learning purposes writing a wallet is a good place to start.

For this tutorial, we will be connecting to the Solana developer network. There we can use fake SOL as much as we want to try things out. So that we don’t lose any real money if something goes wrong.

The completed Solana wallet Rust project can be found on my Github: https://github.com/tmsdev82/solana-wallet-tutorial

I have another Rust tutorial on Solana-related programming: Solana transactions per second: how to with Rust.

Solana Core concepts

Before we start programming our Solana wallet with Rust, let’s talk about Solana itself. Because if we are just starting out it helps to familiarize ourselves with some basic concepts first. We’ll just go through a relatively short summary here. For more detailed information on these concepts please visit the Solana site with official documentation.

What is Solana

Solana is a blockchain technology that aims for cheap, reliable, and fast transactions. One of its main selling points is handling a high number of transactions per second. Significantly more than other blockchain technologies. They also want to keep transaction costs low. Furthermore, Solana supports smart contracts called “programs” to perform actions.

Solana uses a mechanism called proof-of-history to validate blocks and transactions.

Important terms

In this section, we will explain some important terms that we will encounter in this tutorial.

  • Account: A record in the Solana ledger that either holds data or is an executable program.
  • Keypair: A keypair represents a wallet. A wallet has a public key and a private key. The public key is used as the wallet address. The wallet’s private key is used to sign transactions. This private key allows us to transfer Solana from our wallet to a different one.
  • Solana or SOL: the native token of the Solana blockchain. The ticker symbol is “SOL”.
  • Lamports: a unit of token that is a fraction of SOL. Currency values on the Solana blockchain are generally expressed in lamports. Doing this avoids computing with floating point numbers.
  • Block: A continuous set of entries on the ledger covered by a vote.
  • Blockhash: identifier for a block.
  • Instruction: the smallest unit of execution logic in a program. A client can include one or multiple instructions in a single transaction.
  • Program: executable code that interprets the instructions sent inside of each transaction. Also known as smart contracts.
  • Signature: A 64-byte ed25519 signature of R (32-bytes) and S (32-bytes). Each transaction must have at least one signature for theĀ fee account. This first signature can be treated as a transaction id.

For more terminology definitions see the official Solana docs.

Solana wallet Rust project set up

Now let’s begin setting up our project. We’ll create a new project using cargo: cargo new solana-wallet-tutorial.

Then let’s add some dependencies to our project’s Cargo.toml file:

  • clap: A simple to use, efficient, and full-featured Command Line Argument Parser. This will allow us to set up commands to execute specific parts of our program.
  • solana-sdk: Use the Solana SDK Crate to write client-side applications in Rust.
  • solana-client: Client for connecting to a Solana RPC server.
  • tiny-bip39: Library for BIP-39 Bitcoin mnemonic codes. Tiny-bip39 is a fork of the bip39 crate with fixes to v0.6. Rust implementation of BIP-0039.
  • chrono: Date and time library for Rust. We will use this to convert timestamps into a human-readable format.
[package]
name = "solana-wallet-tutorial"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = {version = "4.0.26", features = ["derive"]}
solana-sdk = "1.14.5"
solana-client = "1.14.5"
tiny-bip39 = "0.8.2"
chrono = "0.4.22"

Note: we use version 0.8.2 for tin-bip39 here because the 1.0 version is incompatible with the Solana crates.

Get Solana cluster information command

In this section, we will implement our first command: a command for getting the information of the cluster we connect to. This will teach us how to connect to a Remote Procedure Call (RPC) server.

Set up CLI commands structure

Let’s start by defining a struct for our command line interface and an enum for the subcommands in main.rs:

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[clap(author, version, about, long_about=None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>
}

#[derive(Subcommand)]
enum Commands {
    ClusterInfo
}

fn main() {
    let cli = Cli::parse();

    match &cli.command {
        Some(Commands::ClusterInfo) => {
            println!("Get cluster info");
        },
        None => {}
    }
}

First, we define a struct for our command line interface (CLI) on lines 3-8. Using the clap macros we can define the properties of the CLI. Meaning, what kind of information is available to view about the program: author, version, etc.

All the parts of our program will be accessed through subcommands defined using the Subcommand macro on line 10. For now, we have only one command defined ClusterInfo but as our program grows we will add more to this Commands enum.

The clap crate will convert this enum name to a kabab case command: cluster-info which we will try out in a moment.

Next, in the main() function we call the parse() function on our Cli struct which creates an instance that has parsed the user’s command input. To extract the command we use a match statement on line 18.

When we can build and run the program using the following command cargo run --release -- cluster-info.

Solana wallet with Rust: first output: get cluster info.
CLI output

After the build is finished we can also run the program again using the following command from the root directory of our project ./target/release/solana-wallet-tutorial cluster-info.

Implement the cluster info command

Now, let’s implement the actual function for retrieving some cluster information and print it to the command prompt or terminal.

For this tutorial, we will get the cluster version, the latest block number, and the block time. We have to bring quite a lot of things into scope for this:

use chrono::{DateTime, NaiveDateTime, Utc};
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    account::from_account, clock::Clock, commitment_config::CommitmentConfig, sysvar,
};

#[derive(Parser)]
#[clap(author, version, about, long_about=None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    ClusterInfo,
}

const SERVER_URL: &str = "https://api.devnet.solana.com";

fn get_cluster_info(client: &RpcClient) {
    let version = client.get_version().unwrap();
    let result = client
        .get_account_with_commitment(&sysvar::clock::id(), CommitmentConfig::finalized())
        .unwrap();

    let (slot, timestamp) = match result.value {
        Some(clock_account) => {
            let clock: Clock = from_account(&clock_account).unwrap();
            (result.context.slot, clock.unix_timestamp)
        }
        None => {
            panic!("Unexpected None");
        }
    };

    
    let datetime = DateTime::<Utc>::from_utc(
        NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(),
        Utc,
    );

    println!("Cluster version: {}", version.solana_core);
    println!(
        "Block: {}, Time: {}",
        slot,
        datetime.format("%Y-%m-%d %H:%M:%S")
    );
}

fn main() {
    let cli = Cli::parse();
    let client = RpcClient::new(SERVER_URL);

    match &cli.command {
        Some(Commands::ClusterInfo) => {
            println!("Get cluster info");
            get_cluster_info(&client)
        }
        None => {}
    }
}

Get cluster information function

Let’s look at the get_cluster_info function we added:

fn get_cluster_info(client: &RpcClient) {
    let version = client.get_version().unwrap();
    let result = client
        .get_account_with_commitment(&sysvar::clock::id(), CommitmentConfig::finalized())
        .unwrap();

    let (slot, timestamp) = match result.value {
        Some(clock_account) => {
            let clock: Clock = from_account(&clock_account).unwrap();
            (result.context.slot, clock.unix_timestamp)
        }
        None => {
            panic!("Unexpected None");
        }
    };

    
    let datetime = DateTime::<Utc>::from_utc(
        NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(),
        Utc,
    );

    println!("Cluster version: {}", version.solana_core);
    println!(
        "Block: {}, Time: {}",
        slot,
        datetime.format("%Y-%m-%d %H:%M:%S")
    );
}

This function takes a reference to an RpcClient object as a parameter. With this object, we can connect to an RPC server and perform actions.

Getting the version of Solana that the servers are running is easy using the get_version() function on line 25. This function returns a RpcVersionInfo object we will use at the end of the function to print the value of version.solana_core.

To get the cluster date we are going to get data from the Clock program. So, we need to know the Id of the program to look it up. We can get this from sysvar::clock::id(), as seen on line 25. We want to get the latest finalized run. So we call get_account_with_commitment and set the commitment to CommitementConfig::finalized().

We then deserialize the resulting data to a sysvar or system account, in this case Clock using from_account on line 30. If we successfully deserialize we can get the UNIX timestamp and also get the slot number.

Then we convert this timestamp to a DateTime object on lines 39-42 so we can present the date and time in a human-readable format at the end of the function.

Solana client connection and calling the get_cluster_info function

Finally, we will update main.rs to make a connection to a RPC cluster and call our new get_cluster_info function. We will connect to the devnet, which is a network for developers:

fn main() {
    let cli = Cli::parse();
    let client = RpcClient::new(SERVER_URL);

    match &cli.command {
        Some(Commands::ClusterInfo) => {
            println!("Get cluster info");
            get_cluster_info(&client)
        }
        None => {}
    }
}

Here on line 54, we create a new RpcClient instance using the SERVER_URL const we defined earlier on line 20 of main.rs: const SERVER_URL: &str = "https://api.devnet.solana.com";

Then finally we call our function get_cluster_info passing in a reference to the client instance on line 59.

Running the program now with the command cargo run --release -- cluster-info will result in the following:

Solana cluster information output: cluster version 1.13.5. Block 178759058, Time: 2022-11-29 09:08:18.
Cluster info output

Get Solana supply command

In this section, we will look at how to get the total and circulating supply of Solana. Again, this is a fairly simple thing to do thanks to the Solana crates. However, this is a nice opportunity to see how easy it is to convert lamport values to SOL.

First, we need to bring a function into the scope and add a new command:

use chrono::{DateTime, NaiveDateTime, Utc};
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    account::from_account, clock::Clock, commitment_config::CommitmentConfig,
    native_token::lamports_to_sol, sysvar,
};

#[derive(Parser)]
#[clap(author, version, about, long_about=None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    ClusterInfo,
    Supply,
}

The Solana SDK crate has a convenient function for Lamport to Sol conversions: lamports_to_sol.

Implementing the get_supply function

Next, let’s implement the function for retrieving and printing the Solana supply information:

fn get_supply(client: &RpcClient) {
    let supply_response = client.supply().unwrap();
    let supply = supply_response.value;

    println!(
        "Total supply: {} SOL\nCirculating: {} SOL\nNon-Circulating: {} SOL",
        lamports_to_sol(supply.total),
        lamports_to_sol(supply.circulating),
        lamports_to_sol(supply.non_circulating)
    );
}

As we can see getting the supply is easy by using the supply() function which returns a Response<RpcSupply> object. We get the RpcSupply object from the value field on line 55. Then we simply pass the relevant fields from that object (total, circulating, non_circulating) to the lamports_to_sol function to get the values in terms of SOL.

Update command processing for supply command

Now we just have to add the Supply command to the match expression in our main() function:

fn main() {
    let cli = Cli::parse();
    let client = RpcClient::new(SERVER_URL);

    match &cli.command {
        Some(Commands::ClusterInfo) => {
            println!("Get cluster info");
            get_cluster_info(&client)
        }
        Some(Commands::Supply) => {
            println!("Get supply info");
            get_supply(&client);
        }
        None => {}
    }
}

Then we can run the program with the following command: cargo run --release -- supply to try out the command. The output should look something like this:

Solana wallet supply command output: total supply, circulating, non-circulating.
Supply command output

Generate wallet keypairs command

In this section, we will make our Solana wallet Rust program an actual Solana wallet by generating keypairs and then using those in other sections for receiving SOL and transferring SOL to a different wallet.

We are going to store the keypairs in a JSON file which can be specified by the user. And we will also let users set a passphrase and the number of words for the recovery mnemonic. This recovery mnemonic can be used to generate the keypair again. For example, in case the keypair file is lost.

New scope items and define the key generation command

Again we need to bring new elements into the scope and add the command for generating keypairs.

use bip39::{Language, Mnemonic, MnemonicType, Seed};
use chrono::{DateTime, NaiveDateTime, Utc};
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    account::from_account,
    clock::Clock,
    commitment_config::CommitmentConfig,
    native_token::lamports_to_sol,
    signature::{keypair_from_seed, write_keypair_file},
    signer::Signer,
    sysvar,
};

#[derive(Parser)]
#[clap(author, version, about, long_about=None)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    ClusterInfo,
    Supply,
    KeyGen {
        #[arg(short, long, help = "Output file path for keypair file.")]
        output: String,
        #[arg(
            short,
            long,
            default_value_t = 12,
            help = "How many words to generate for the mnemonic. Valid values are: 12, 15, 18, 21, and 24."
        )]
        mnemonic_word_count: u32,
        #[arg(short, long, help = "Passphrase to use for extra security.")]
        passphrase: Option<String>,
    },
}

We bring items from the tiny-bip39 crate into scope for the mnemonic and generating a seed for the keypairs based on that. Also a function from the Solana SDK for writing a Keypair object to a file and generating keys from a seed.

The key generation command is defined on lines 26-38. As mentioned we have parameters for the output file path, how many words the mnemonic should be, and an optional passphrase for extra security.

We specify some additional information for the parameters of this command in the form of: #[arg(short, long, help = "")].

  • short: enable shortened param name, it takes the first letter of the variable name. For example, output becomes -o in this command.
  • long: enabled long param name. For example, output becomes --output in this command. And mnemonic_word_count becomes --mnemonic-word-count.
  • help: help text to display when user types --help at the end of the command.

Implementing the generate_keypair function

Let’s look at the function for generating the keypair. We want to generate the keypair based on a number of words (minimum 12) so that we can recover our keys using that mnemonic in the future when needed.

So we first generate the mnemonic, then a seed based on that and a passphrase, and then generate the keys from that seed:

fn generate_keypair(output_path: &str, mnemonic_word_count: usize, passphrase: &Option<String>) {
    let mnemonic_type = MnemonicType::for_word_count(mnemonic_word_count).unwrap();
    let mnemonic = Mnemonic::new(mnemonic_type, Language::English);

    let seed = match passphrase {
        Some(phrase) => Seed::new(&mnemonic, phrase),
        None => Seed::new(&mnemonic, ""),
    };

    let keypair = keypair_from_seed(seed.as_bytes()).unwrap();
    write_keypair_file(&keypair, output_path).unwrap();

    println!("Mnemonic: {:?}", mnemonic);
    println!("Public key: {}", &keypair.pubkey());
}

First, we generate the mnemonic based on word count and a language on lines 85-86. Languages other than English are supported and we could make it an option for the KeyGen command. However, to keep things simple we hard-code English here.

Then on lines 88-91, we check to see if there is a passphrase or not. Depending on that we generate the seed with an empty passphrase or not.

Next, we call keypair_from_seed and we have to pass in the seed as a byte array to get a Keypair object.

Finally, we write the keypair to the specified file using write_keypair_file to store the keypair for later use. Also, we print the mnemonic and public key on lines 96-97. Since the public key is also the wallet’s address.

Update command processing for KeyGen command

Now let’s add the command execution to our match expression in main():

fn main() {
    let cli = Cli::parse();
    let client = RpcClient::new(SERVER_URL);

    match &cli.command {
        Some(Commands::ClusterInfo) => {
            println!("Get cluster info");
            get_cluster_info(&client)
        }
        Some(Commands::Supply) => {
            println!("Get supply info");
            get_supply(&client);
        }
        Some(Commands::KeyGen {
            output,
            mnemonic_word_count,
            passphrase,
        }) => {
            println!("Generate keys, output to: {}", output);
            generate_keypair(output, *mnemonic_word_count as usize, passphrase);
        }
        None => {}
    }
}

Nothing note worthy here, we just pass all parameters directly to generate_keypair.

Now we can run the command using cargo run --release -- key-gen --output ./keypair.json --mnemonic-word-count 12. In this case, we don’t specify a passphrase. Executing this command will put the keypair.json file in the root directory of our project.

The output should look something like this:

Rust Solana wallet keypair generation output: mnemonic and public key.
Solana wallet key generation output

We will use this wallet to test the remaining commands we’ll be implementing.

Get Solana balance command

Let’s implement a command for checking the balance at an address. With this, we will be able to check the balance of our wallet. Which will be 0 for now, but we will airdrop SOL into it in the next section.

We should implement the command such that it can use either a wallet file or an address string to retrieve the balance for convenience’s sake.

Scopes and command updates

Let’s update the top of main.rs:

use bip39::{Language, Mnemonic, MnemonicType, Seed};
use chrono::{DateTime, NaiveDateTime, Utc};
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    account::from_account,
    clock::Clock,
    commitment_config::CommitmentConfig,
    native_token::lamports_to_sol,
    pubkey::Pubkey,
    signature::{keypair_from_seed, read_keypair_file, write_keypair_file},
    signer::Signer,
    sysvar,
};
use std::str::FromStr;

Here we bring pubkey::Pubkey and FromStr into scope, so that we can get a Pubkey object from a string.

Next, the command definition:

#[derive(Subcommand)]
enum Commands {
    ClusterInfo,
    Supply,
    KeyGen {
        #[arg(short, long, help = "Output file path for keypair file.")]
        output: String,
        #[arg(
            short,
            long,
            default_value_t = 12,
            help = "How many words to generate for the mnemonic. Valid values are: 12, 15, 18, 21, and 24."
        )]
        mnemonic_word_count: u32,
        #[arg(short, long, help = "Passphrase to use for extra security.")]
        passphrase: Option<String>,
    },
    Balance {
        #[arg(group = "input")]
        address: Option<String>,
        #[arg(long, group = "input")]
        wallet_file: Option<String>,
    },
}

Using #[arg(group = "input")] we can indicate that one of the elements in the group should be entered as a parameter for the command. So either an address or a wallet file should be entered.

Implementing the get_balance function

This is another simple function, only a few lines of code:

fn get_balance(address: &str, client: &RpcClient) {
    let pubkey = Pubkey::from_str(address).unwrap();
    let balance = client.get_balance(&pubkey).unwrap();

    println!("Balance for {}: {}", address, lamports_to_sol(balance));
}

The get_balance function takes in an address as string slice, and a reference to the RpcClient object.

We convert the address string slice into a Pubkey object on line 109 and then get the balance for this address using the RPC client function get_balance passing in a reference to the Pubkey object.

Finally, we convert the lamports value into Sol and print the result on line 112.

Updating the command processing by adding Balance

Let’s add the Balance command to the match expression in main():

fn main() {
    let cli = Cli::parse();
    let client = RpcClient::new(SERVER_URL);

    match &cli.command {
        Some(Commands::ClusterInfo) => {
            println!("Get cluster info");
            get_cluster_info(&client)
        }
        Some(Commands::Supply) => {
            println!("Get supply info");
            get_supply(&client);
        }
        Some(Commands::KeyGen {
            output,
            mnemonic_word_count,
            passphrase,
        }) => {
            println!("Generate keys, output to: {}", output);
            generate_keypair(output, *mnemonic_word_count as usize, passphrase);
        }
        Some(Commands::Balance {
            address,
            wallet_file,
        }) => {
            if let Some(address) = address {
                println!("Get balance for address: {}", address);
                get_balance(address, &client);
            } else if let Some(wallet_path) = wallet_file {
                println!("Get balance for Wallet file: {}", wallet_path);
                let keypair = read_keypair_file(wallet_path).unwrap();
                get_balance(&keypair.pubkey().to_string(), &client);
            }
        }
        None => {}
    }
}

Here we do a little bit more work than for the other commands. We first have to determine if the user put in an address or a wallet file location. If we got a wallet file location we read the file using read_keypair_file, a function from the Solana SDK, and then pass the public key to the get_balance function.

Now we can run the command cargo run --release -- balance --wallet-file "./keypair.json" to check the balance of our newly created wallet:

Output for getting balance of our wallet: 0
0 balance for newly created wallet

As expected the balance is 0. Let’s try the command with a different wallet address. Since we already built the latest version of our program we can run it from the release directory:

./target/release/solana-wallet-tutorial balance "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM"

Display Solana balance for Solana addresses.
Display balance of other Solana addresses

Next, let’s do something about the 0 balance of our wallet.

Airdrop to a Solana wallet address

In this section, we will airdrop (fake) Sol into our Solana Rust wallet. Note that we can only do this on test and dev nets, etc. This is not possible on the main network where real tokens are used.

Bringing new items into scope

We have some new “uses” for converting Sol to Lamports and some things for printing to stdout and other things:

use bip39::{Language, Mnemonic, MnemonicType, Seed};
use chrono::{DateTime, NaiveDateTime, Utc};
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    account::from_account,
    clock::Clock,
    commitment_config::CommitmentConfig,
    native_token::{lamports_to_sol, sol_to_lamports},
    pubkey::Pubkey,
    signature::{keypair_from_seed, read_keypair_file, write_keypair_file},
    signer::Signer,
    sysvar,
};
use std::{
    io::{self, Write},
    str::FromStr,
};
use std::{thread, time};

Updating the commands

Let’s add the airdrop command to the Commands enum:

#[derive(Subcommand)]
enum Commands {
    ClusterInfo,
    Supply,
    KeyGen {
        #[arg(short, long, help = "Output file path for keypair file.")]
        output: String,
        #[arg(
            short,
            long,
            default_value_t = 12,
            help = "How many words to generate for the mnemonic. Valid values are: 12, 15, 18, 21, and 24."
        )]
        mnemonic_word_count: u32,
        #[arg(short, long, help = "Passphrase to use for extra security.")]
        passphrase: Option<String>,
    },
    Balance {
        #[arg(group = "input")]
        address: Option<String>,
        #[arg(long, group = "input")]
        wallet_file: Option<String>,
    },
    Airdrop {
        #[arg(short, long)]
        address: String,
        #[arg(short, long)]
        sol: f64,
    },
}

We just have the address and Sol amount as parameters for this command.

Implement the airdrop_sol function

Next, let’s implement the function for doing airdrops we’ll call it airdrop_sol. It will take and address and float, the amount of Sol, as parameters:

fn airdrop_sol(address: &str, sol: f64, client: &RpcClient) {
    let lamports = sol_to_lamports(sol);
    let pubkey = Pubkey::from_str(address).unwrap();
    let signature = client.request_airdrop(&pubkey, lamports).unwrap();

    let wait_milis = time::Duration::from_millis(100);
    print!("Waiting to confirm");
    io::stdout().flush().unwrap();
    loop {
        if let Ok(confirmed) = client.confirm_transaction(&signature) {
            if confirmed {
                println!("\nAirdrop to {}: {}", address, confirmed);
                break;
            }
        }
        print!(".");
        io::stdout().flush().unwrap();
        thread::sleep(wait_milis);
    }
}

First, we convert the Sol value to Lamports because Solana programs on the blockchain use lamports for values.

Then we create a Pubkey object from the address string parameter on line 123.

Next, we call request_airdrop on the client object. This will send the request but will not wait for confirmation. That is why we enter a loop on lines 128-139 to wait for the confirmation. In the meantime, to make the process of waiting visible we print “.” each round of the loop. To print output in real time we need to call io::stdout().flush().

Updating the command processing for airdrop

Finally, we have to update the match block where the commands are processed in main():

fn main() {
    let cli = Cli::parse();
    let client = RpcClient::new(SERVER_URL);

    match &cli.command {
        Some(Commands::ClusterInfo) => {
            println!("Get cluster info");
            get_cluster_info(&client)
        }
        Some(Commands::Supply) => {
            println!("Get supply info");
            get_supply(&client);
        }
        Some(Commands::KeyGen {
            output,
            mnemonic_word_count,
            passphrase,
        }) => {
            println!("Generate keys, output to: {}", output);
            generate_keypair(output, *mnemonic_word_count as usize, passphrase);
        }
        Some(Commands::Balance {
            address,
            wallet_file,
        }) => {
            if let Some(address) = address {
                println!("Get balance for address: {}", address);
                get_balance(address, &client);
            } else if let Some(wallet_path) = wallet_file {
                println!("Get balance for Wallet file: {}", wallet_path);
                let keypair = read_keypair_file(wallet_path).unwrap();
                get_balance(&keypair.pubkey().to_string(), &client);
            }
        }
        Some(Commands::Airdrop { address, sol }) => {
            println!("Airdrop {} SOL to {}", sol, address);
            airdrop_sol(address, *sol, &client);
        }
        None => {}
    }
}

Now we can run the command with our wallet’s address. Make sure you use your own value here but mine is:

cargo run --release -- airdrop -a GJWwf5cNWj4o6A6kvqqTSkr7nkb84w3tYQEMkiXcdBuZ -s 2

Because the airdrop is free there are limits to how often we can get some. Sometimes we will get an error. If we get an error we should just try and try again until the operation succeeds:

Error getting Sol airdropped.
Airdrop error

If the request succeeds the output looks something like this:

Airdrop to Solana address succeeded output.
Airdrop succeeded

Transfer Solana from our Rust wallet to another wallet

In this final section, we will write functionality to transfer Sol from our Solana wallet to a different address with our Rust program.

To do a transfer we need to create an instruction for transfer and put it in a transaction. We then have to sign that transaction with our private key to confirm that the transaction is from us and that we are allowing it.

Bringing new items into scope

Again we have to bring new items into scope to complete our Solana wallet Rust program. This time, for creating instructions and transactions:

use bip39::{Language, Mnemonic, MnemonicType, Seed};
use chrono::{DateTime, NaiveDateTime, Utc};
use clap::{Parser, Subcommand};
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    account::from_account,
    clock::Clock,
    commitment_config::CommitmentConfig,
    native_token::{lamports_to_sol, sol_to_lamports},
    pubkey::Pubkey,
    signature::{keypair_from_seed, read_keypair_file, write_keypair_file, Keypair},
    signer::Signer,
    system_instruction, sysvar,
    transaction::Transaction,
};
use std::{
    io::{self, Write},
    str::FromStr,
};
use std::{thread, time};

Updating the commands to add Transfer

Next, we’ll add the Transfer command to our Commands enum:

#[derive(Subcommand)]
enum Commands {
    ClusterInfo,
    Supply,
    KeyGen {
        #[arg(short, long, help = "Output file path for keypair file.")]
        output: String,
        #[arg(
            short,
            long,
            default_value_t = 12,
            help = "How many words to generate for the mnemonic. Valid values are: 12, 15, 18, 21, and 24."
        )]
        mnemonic_word_count: u32,
        #[arg(short, long, help = "Passphrase to use for extra security.")]
        passphrase: Option<String>,
    },
    Balance {
        #[arg(group = "input")]
        address: Option<String>,
        #[arg(long, group = "input")]
        wallet_file: Option<String>,
    },
    Airdrop {
        #[arg(short, long)]
        address: String,
        #[arg(short, long)]
        sol: f64,
    },
    Transfer {
        #[arg(short, long)]
        from_wallet: String,
        #[arg(short, long)]
        to: String,
        #[arg(short, long)]
        sol: f64,
    },
}

We have a parameter for the wallet file location: from_wallet and a recipient address: to and then the amount to transfer: sol.

Implementing the transfer_sol function

As mentioned at the start of this section we have to create a transaction with a transfer instruction, then sign that transaction with our private key before we can finally send it.

Here is the code:

fn transfer_sol(client: &RpcClient, keypair: &Keypair, to_key: &str, sol_amount: f64) {
    let to_pubkey = Pubkey::from_str(to_key).unwrap();
    let lamports = sol_to_lamports(sol_amount);
    let transfer_instruction =
        system_instruction::transfer(&keypair.pubkey(), &to_pubkey, lamports);

    let latest_blockhash = client.get_latest_blockhash().unwrap();

    let transaction = Transaction::new_signed_with_payer(
        &[transfer_instruction],
        Some(&keypair.pubkey()),
        &[keypair],
        latest_blockhash,
    );

    let wait_milis = time::Duration::from_millis(100);
    print!("Waiting to confirm");
    io::stdout().flush().unwrap();

    match client.send_transaction(&transaction) {
        Ok(signature) => loop {
            if let Ok(confirmed) = client.confirm_transaction(&signature) {
                if confirmed {
                    println!("\nTransfer of sol was confirmed");
                    break;
                }
            }
            print!(".");
            io::stdout().flush().unwrap();
            thread::sleep(wait_milis);
        },
        Err(e) => {
            println!("Error transferring sol: {}", e);
        }
    }
}

First, we have to create a Pubkey object from the string slice parameter to_key on line 156. Then we convert the Sol float value to Lamports on the next line because the transfer instructions expect the value in Lamports.

Next, we create the transfer instruction using system_instruction::transfer.

Then we create the transaction on lines 163-168. Because there are costs associated with transfer transactions we use Transaction::new_signed_with_payer to create the transaction. And we are the ones that have to pay.

This Transaction::new_signed_with_payer function takes an array of instructions, in our case we only have one. Then a public key, representing the transaction fee payer’s address. And the keypair and the latest block hash are required for signing the transaction.

Next, we send the transaction on line 174. We then enter a loop on line 175 to wait for the transaction to be confirmed, like we did for the airdrop call as well.

The client.confirm_transaction function will return Ok with a boolean value when the transaction is confirmed. While waiting we print some text to the terminal to show activity to the users indicating that the program is still working. Otherwise, it would look like the program is stuck for a while doing nothing.

Transferring Sol from one wallet to another

Let’s test our new transfer command. The easiest way to do this is to create another wallet next to the one we already have and then transfer Sol between those wallets.

So, we’ll create another wallet using the following command: ./target/release/solana-wallet-tutorial key-gen -o ./wallet2.json -m 12.

The output should look something like this:

Output Solana Rust wallet creation number 2.
Wallet 2 creation

This will create another JSON file in the root directory and display the new public key. We need this public key for the following commands.

Now let’s check the balance real quick: ./target/release/solana-wallet-tutorial balance "2bfDxD1h9jpaep9zgaHtZauv1EBmqoq6UVsFbWFYExCr"

Solana wallet 2 balance: 0
Wallet 2 balance

Now we’ll transfer Sol from our first wallet to this second one. But first, let’s make sure the program is built with the latest code: cargo build --release.

Then let’s run the command: ./target/release/solana-wallet-tutorial transfer --from-wallet ./keypair.json --to 2bfDxD1h9jpaep9zgaHtZauv1EBmqoq6UVsFbWFYExCr --sol 0.05

Output showing result of transfer command.
Transfer Sol output

If we check our wallet2 address now we should see a balance of 0.05 Sol:

./target/release/solana-wallet-tutorial balance "2bfDxD1h9jpaep9zgaHtZauv1EBmqoq6UVsFbWFYExCr"

Balance of wallet 2 after transfer: 0.05 Sol
Balance of wallet 2 after transfer

Let’s also look at the balance of our first wallet. We should see that balance has been reduced a little more than 0.05 since we had to pay transaction costs:

./target/release/solana-wallet-tutorial balance --wallet-file ./keypair.json

Output showing the balance in our original wallet has been reduced a bit more than 0.05 due to transaction fees.
Original wallet balance output

Conclusion

In this tutorial, we learned some basics of interacting with the Solana blockchain by building a Solana wallet Rust program. We learned a bit about transactions, instructions, getting cluster information, airdropping Sol, and getting the balance from a specified Solana address.

So now we can create our own wallet and perform transfer transactions.

The completed Solana wallet Rust project can be found on my Github: https://github.com/tmsdev82/solana-wallet-tutorial

If you enjoyed this tutorial, also check out my other Solana-related tutorial: Solana transactions per second: how to with Rust.

Please follow me on Twitter to get updates on more cryptocurrency programming-related tutorials:

Leave a Reply

Your email address will not be published. Required fields are marked *