Scaffold Series - Part 2 Wallet Balance

Become a Solana developer. Let's unroll The Solana Path together with this top-down approach tutorial. In this part, we will learn how to fetch the wallet balance.

Scaffold Series - Part 2 Wallet Balance

This is part 2 of the Scaffold Series

🍀 What Will I Get From This?

  • [ ] Don't be an island and meet your new companion for your Solana journey.
  • [ ] Build a faucet to get some testing SOL.
  • [ ] Learn about Lamports and SOL.
  • [ ] See with your own eyes what is happening in the Solana blockchain.
  • [ ] Meet @solana/web3.js, your friend, to communicate with Solana.

🎬 Previously In Scaffold Starter...

Welcome back! In the previous episode, we connected the user's wallet to our dapp. Now that we got access to the user's wallet, we can actually start interacting with the Solana blockchain; this is where the web3 line starts.

🚀 What Are We Building Today?

Today, we will display the user's SOL balance; we will:

  • Learn how to use Solana Javascript SDK.
  • Learn how to find information with the Solana Cookbook.
  • Explore the blockchain information with explorers.

Brainstorm

We want to get the user's wallet balance, which leads to the question, where is the balance stored? Is that in the wallet? Is that in the blockchain?

One misconception when starting web3 is that your tokens live inside your wallet. A Wallet App, such as Phantom or Sollet, fetches data from the blockchain to show you additional information.

So, a Wallet App is actually just a dapp that leverages your private key to do different things:
  • The simplest one, just protect your private key with a passphrase
  • Then, for utility, they also fetch some information for your key, such as token balance
  • More advanced, they can also start proposing token swaps, etc.

So, wallet balances are really just data in the blockchain and are stored by Solana nodes. It is data that you can query in Solscan or Solana explorer. Using the explorers, you can also see my wallet balance: https://explorer.solana.com/address/E35325pbtxCRsA4uVoC3cyBDZy8BMpmxvsvGcHNUa18k?cluster=devnet.

Introducing The Cookbook

Ok, great, but how do we actually do that in our dapp? First, let me introduce you to your best companion in your Solana journey: https://solanacookbook.com!

The Solana Cookbook is your reference; every time you want to find information about how to achieve something, there must have a recipe created by the community.

So, this is how we can read a wallet balance: https://solanacookbook.com/references/accounts.html#how-to-get-account-balance

import { clusterApiUrl, Connection, PublicKey, LAMPORTS_PER_SOL } from "@solana/web3.js";

const connection = new Connection(clusterApiUrl("devnet"), "confirmed");

let wallet = new PublicKey("G2FAbFQPFa5qKXCetoFZQEvF9BVvCKbvUZvodpVidnoY");
let balance = await connection.getBalance(wallet);
console.log(`${balance / LAMPORTS_PER_SOL} SOL`);

Thanks to the @solana/web3.js javascript client library, we can communicate with the Solana blockchain via its RPC nodes. We have already added the library in the starter project's package.json:

  "dependencies": {
     ...
    "@solana/web3.js": "^1.31.0",
  }

But if you don't have it yet, you can manually install it with:

yarn add @solana/web3.js

Fetching Users' Wallets Information

Enough chatting, let's get coding! Open src/views/home/index.tsx

🧐 If it is public information in the blockchain, why do we even need to get the connection to users' wallets then? The reason is to get their public key. As a user, would you want the user to paste its public key in a text field? No? Me neither!

Let's try fetching information from the users' wallets (please note that I omitted the already existing HTML to ease your eyes):

import { useWallet } from '@solana/wallet-adapter-react';

...
export const HomeView: FC = ({ }) => {
  const wallet = useWallet();
  const balance = 0;
  
  ...
  
  return (

    <div className="md:hero mx-auto p-4">
      <div className="md:hero-content flex flex-col">
        ...
        <h4 className="md:w-full text-center text-slate-300 my-2">
          <p>Simply the fastest way to get started.</p>
          Next.js, tailwind, wallet, web3.js, and more.
        </h4>
        ... 
        <div className="text-center"> // <-- NEW
          {wallet.publicKey && <p>Public Key: {wallet.publicKey.toBase58()}</p>}
          {wallet && <p>SOL Balance: {(balance || 0).toLocaleString()}</p>}
        </div>       
      </div>
    </div>
  );
};

If you haven't missed a closing HTML tag 😁, this is what we should see:

Funding Our Wallet

We will take a little detour to fund our wallets. Indeed if our SOL balance is always 0, we won't know if our balance feature works 😅.

The easiest way at this stage is to use faucets:

(Optional) But Solana also has a secret weapon; there is another option that we can use with the Solana CLI. Feel free to skip this part if you haven't installed the CLI yet. We will install it in later chapters anyway.

While in other blockchains, you would use a faucet, Solana has a handy little tool called the Solana CLI. Let's install the Solana CLI: https://solanacookbook.com/getting-started/installation.html#install-cli.

Once this is done, airdrop yourself some sol by using the Public Key we displayed in the UI:

Solana airdrop 1 YOUR_WALLET_PUBLIC_KEY
Requesting airdrop of 1 SOL
Signature: QZNGpV2xSC2VSUrhFqXDs3rC1EqtX71RTcqDNhVPZBhpTLKLeycSwFRsa2oYiFiSHYorRAXrsGfVuyL5BVXfKjt

2 SOL
In Solana, your wallet public key is your public address.

We can verify the balance with:

Solana balance YOUR_WALLET_PUBLIC_KEY
1 SOL

So the CLI is a convenient tool to interact with the blockchain without having to open a website or craft json requests! Anyway, it works on the command line, but we want to display it in our dapp. Let's continue.

Getting The Wallet Balance

Now that our wallet is funded, we can remove the hardcoded balance = 0 and replace it with actual data that we will query from the blockchain with @solana/web3.js

Let's, take a look back at the cookbook reference if you forgot: https://solanacookbook.com/references/accounts.html#how-to-get-account-balance. We just need to call the getBalance function:

let balance = await connection.getBalance(publicKey);

So we need:

  1. The wallet publicKey: that's easy. We have already displayed it before!
  2. Create a connection.

Let's try implementing it:

...
import { FC, useEffect, useState } from 'react';
import { Connection, PublicKey } from '@solana/web3.js';


export const HomeView: FC = ({ }) => {
  const wallet = useWallet();
  const { connection } = useConnection();
  const [balance, setBalance] = useState(0);

  const getUserSOLBalance = async (publicKey: PublicKey, connection: Connection) => {
    let balance = await connection.getBalance(publicKey);
    setBalance(balance)
  }

  useEffect(() => {
    if (wallet.publicKey) {
      getUserSOLBalance(wallet.publicKey, connection)
    }
  }, [wallet.publicKey, connection, getUserSOLBalance])

  return (
    ...
  );
};


Don't be afraid of the sudden amount of code. We are just doing some react magic using useEffect and useState to manage the view state. The exciting code resides in getUserSOLBalance.

Success!

Ok, maybe I don't have that much SOL in my balance. What's going on? Well, usually, blockchains do not store decimals. Instead, we use big numbers to represent smaller units of tokens. For Solana, the smallest unit is expressed in lamports.

1 SOL is 1_000_000_000 LAMPORTS
1 SOL * 10^9 = LAMPORTS

A Lamport has a value of 0.000000001 SOL, but don't worry, you don't have to hardcode this everywhere 😁. Just divide by LAMPORTS_PER_SOL:

import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';


export const HomeView: FC = ({ }) => {
  const getUserSOLBalance = async (publicKey: PublicKey, connection: Connection) => {
    let balance = await connection.getBalance(publicKey);
    setBalance(balance / LAMPORTS_PER_SOL)
  }
  ...
}

Better!

Lamports To SOL

😅 Why did we just lose 5mn of our lives to mention this LAMPORTS_PER_SOL division instead of adding it directly? In the future, when you will do your calculations and divisions, remember to always use lamports to avoid decimal approximation! This is a common error to convert the value to decimals and lose some decimals in the process. Also, as you pass the value from variables to variables, you will forget where you did the rounding and where you did not.

💡 Quiz

  • [ ] If you need to cook a new Solana feature, what is the name of your new companion?
  • [ ] What is the easiest way to fund your account for testing?
  • [ ] When is it acceptable to convert or divide by LAMPORTS_PER_SOL?
  • [ ] How can you explore information about the Solana blockchain without coding?
  • [ ] To fetch the wallet information, should you use WalletAdapter or @solana/web3.js?
  • [ ] What library can you use to communicate with Solana RPC?

🏆 Achievement - Balance

The final code is here: https://github.com/solana-labs/dapp-scaffold/commit/bc8387c5267307be5bbbc2fba80942d28ddbe2ce

📙 Cookbook References

🎙 Your Turn To Get The Mic!

All right, enough talking for me. I am handing over the mic to you! If you accept it, your mission is to implement the Airdrop Button. We funded our wallet with the faucets or the Solana CLI. Instead of using external tools, how about we implement this feature right inside our dapp? This would be useful for us as devs as we continue to work on this Scaffold DApp. We might as well make our life easier and add a button directly in the UI to fund our wallets.

Do not worry too much about the UI. Just focus on making it work first. The question is, how do we start?

  • [ ] Add a button with a callback to console.log("Airdrop requested!")..
  • [ ] Use the cookbook to find the recipe we are looking for. (Hint: look for "airdrop" or
    "Getting SOL").
  • [ ] Implement the new recipe with your button callback.

Don't look for the solution before spending at least trying for 10 minutes 😉

Solution To The Airdrop Challenge

First, insert the UI component into views/home/index.tsx:

import { RequestAirdrop } from '../../components/RequestAirdrop';

...

 <div className="text-center">
	<RequestAirdrop />
	{wallet.publicKey && <p>Public Key: {wallet.publicKey.toBase58()}</p>}
	{wallet && <p>SOL Balance: {(balance || 0).toLocaleString()}</p>}
</div>

Then create that new component in components/RequestAirdrop.tsx

import { FC, useCallback } from 'react';
import { notify } from "../utils/notifications";
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { LAMPORTS_PER_SOL, TransactionSignature } from '@solana/web3.js';
import getUserSOLBalance from '../stores/useUserSOLBalanceStore';


export const RequestAirdrop: FC = () => {
    const { connection } = useConnection();
    const { publicKey } = useWallet();
    const { getUserSOLBalance } = useUserSOLBalanceStore();

    const onClick = useCallback(async () => {
        if (!publicKey) {
            console.log('error', 'Wallet not connected!');
            notify({ type: 'error', message: 'error', description: 'Wallet not connected!' });
            return;
        }

        let signature: TransactionSignature = '';

        try {
            signature = await connection.requestAirdrop(publicKey, 1 * LAMPORTS_PER_SOL);
            await connection.confirmTransaction(signature, 'confirmed');
            notify({ type: 'success', message: 'Airdrop successful!', txid: signature });
			getUserSOLBalance(publicKey, connection);
        } catch (error: any) {
            notify({ type: 'error', message: `Airdrop failed!`, description: error?.message, txid: signature });
            console.log('error', `Airdrop failed! ${error?.message}`, signature);
        }
    }, [publicKey, connection, getUserSOLBalance]);

    return (
        <div>
            <button
                className="px-8 m-2 btn animate-pulse bg-gradient-to-r from-[#9945FF] to-[#14F195] hover:from-pink-500 hover:to-yellow-500 ..."
                onClick={onClick}
            >
                <span>Airdrop 1 </span>
            </button>
        </div>
    );
};

Pretty straightforward, isn't it? Aside from the notify that we put here to get more feedback on what is going on, the trick is about refreshing the user's balance.

From there, it is more about react state management. So we refactored the getUserSOLBalance into its own function, not just a function but also a store as we need to propagate that state back to other components.

Create that store in stores/getUserSOLBalance.tsx:

import create, { State } from 'zustand'
import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js'

interface UserSOLBalanceStore extends State {
  balance: number;
  getUserSOLBalance: (publicKey: PublicKey, connection: Connection) => void
}

const useUserSOLBalanceStore = create<UserSOLBalanceStore>((set, _get) => ({
  balance: 0,
  getUserSOLBalance: async (publicKey, connection) => {
    let balance = 0;
    try {
      balance = await connection.getBalance(
        publicKey,
        'confirmed'
      );
      balance = balance / LAMPORTS_PER_SOL;
    } catch (e) {
      console.log(`error getting balance: `, e);
    }
    set((s) => {
      s.balance = balance;
      console.log(`balance updated, `, balance);
    })
  },
}));

export default useUserSOLBalanceStore;

We have chosen to use zustand here for state management, but it is really up to you what solution to choose.

Finally, let's do the cleanup in index.tsx to reuse this UserSOLBalanceStore, and voila:

...
// Store
import useUserSOLBalanceStore from '../../stores/useUserSOLBalanceStore';

export const HomeView: FC = ({ }) => {
  ...
  const { getUserSOLBalance } = useUserSOLBalanceStore()
  const balance = useUserSOLBalanceStore((s) => s.balance)

  useEffect(() => {
    if (wallet.publicKey) {
      getUserSOLBalance(wallet.publicKey, connection)
    }
  }, [wallet.publicKey, connection, getUserSOLBalance])
  
  ...
}


🎬 See You In Next Episode


Where To Ask Questions?

How To Stay Up To Date With Solana?