Scaffold Series - Part 3 Sending SOL

, by mwrites
Scaffold Series - Part 3 Sending SOL

This is part 3 of the Scaffold Series

🍀 What Will I Get From This?

  • [ ] Learn where to fish for code examples.
  • [ ] Learn how to send some SOL.
  • [ ] Travel the world with Transactions.
  • [ ] Meet the Solana Native Programs, aka the SNP Crew.

🎬 Previously In Scaffold Starter...

Nice to see you again, friend 😁 ! Now we are transacting!!! (ahem... I mean talking). So let's see:

  • We connected the user wallet. ✅
  • We funded the user wallet. ✅

Now that we have a wallet connected with funds, we can actually start putting our footprint on the blockchain. Instead of just querying data, we can start modifying the blockchain and add data to it with transactions. This is where the web3 line starts!

🚀 What Are We Building Today?

Our goal is to send some SOL to another user:

The War Room

Welcome to the War Room, where we plan our next feature. Instead of throwing code to your face out of nowhere, I want to pair program this with you this time.

How do we start? Let's take advantage of the companion we introduced before. Instead of googling it, we will cookglig it. Ok, that's weird. Anyway! Head to https://solanacookbook.com and search for (or Command+K) "send SOL".

... (Don't look)

The recipe is here: https://solanacookbook.com/references/basic-transactions.html#how-to-send-sol.

Applying The Cookbook Recipe

What does the above gif show? It seems we will need a button to trigger this send SOL transaction, and since we are clean devs, we will make a dedicated <SendTransaction /> component. Let's start by adding this component to our home view /views/home/index.tsx:

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


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

Nothing crazy here. We just import our soon-to-be-created SendTransaction /> component and add it below the <RequestAirdrop /> button that we already had.

Now let's create that <SendTransaction /> component in components/SendTransaction.tsx:

...
import { WalletNotConnectedError } from '@solana/wallet-adapter-base';
import { Keypair, SystemProgram, Transaction } from '@solana/web3.js';


export const SendTransaction: FC = () => {
    const { connection } = useConnection();
    const { publicKey, sendTransaction } = useWallet();

    const onClick = useCallback(async () => {
        if (!publicKey) throw new WalletNotConnectedError();

        const destAddress = Keypair.generate().publicKey; // some random dest
        const amount = 1 * LAMPORTS_PER_SOL; // hardcoded to 1 SOL for now
    
        const transaction = new Transaction().add(
          SystemProgram.transfer({
            fromPubkey: publicKey,
            toPubkey: destAddress,
            lamports: amount,
          })
        );
    
        const signature = await sendTransaction(transaction, connection);
        await connection.confirmTransaction(signature, 'processed');
      }, [publicKey, sendTransaction, connection]);
    
    
    return (
        ...
    );
};

Nothing's happening? Let's rewrite this with some error handling and connect it to our notify UI:

...
import { WalletNotConnectedError } from '@solana/wallet-adapter-base';
import { Keypair, SystemProgram, Transaction } from '@solana/web3.js';
import { notify } from "../utils/notifications";


export const SendTransaction: FC = () => {
    const { connection } = useConnection();
    const { publicKey, sendTransaction } = useWallet();

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

        let signature: TransactionSignature = '';
        try {
            // some random dest
            const destAddress = Keypair.generate().publicKey; 
            const amount = 1 * LAMPORTS_PER_SOL;

            const transaction = new Transaction().add(
                SystemProgram.transfer({
                    fromPubkey: publicKey,
                    toPubkey: destAddress,
                    lamports: amount,
                })
            );

            signature = await sendTransaction(transaction, connection);

            await connection.confirmTransaction(signature, 'confirmed');
            notify({ type: 'success', message: 'Transaction successful!', txid: signature });
        } catch (error: any) {
            notify({ type: 'error', message: `Transaction failed!`, description: error?.message, txid: signature });
            console.log('error', `Transaction failed! ${error?.message}`, signature);
            return;
        }
    }, [publicKey, notify, connection, sendTransaction]);
    
    
    return (
        ...
    );
};

Result:

🏆 Achievement? Not So Soon!

Thanks to the cookbook, we just had to copy-paste the code. But we are better than copy-pasting, aren't we? Aren't we? Aren't we?

The cookbook is awesome, but what I want you to get from this is not just how to look for code. We are here to dig deeper. Let's look again at the recipe and see what can we learn about it:

const transaction = new Transaction().add(
    SystemProgram.transfer({
        fromPubkey: publicKey,
        toPubkey: destAddress,
        lamports: amount,
    })
);
  • SystemProgram: looks interesting; what is this?
  • Transaction: we hadn't seen this before, or did we?

Feel free to take some water, get a deep breath, and congratulate yourself for getting things done. And be ready to dive deeper into these two concepts when you return.

Let's Dig - The System Program

What is that SystemProgram? Solana comes equipped with a number of programs that serve as core building blocks for on-chain interactions. These programs are divided into Native Programs and Solana Program Library (SPL) Programs.

Basically, it means that every Solana node runs some programs (also called smart contracts in other blockchains) out of the box.

💡 Think: What is the most basic functionality for a blockchain node?

Hint: Think about what do people do with their wallets

... (don't look below)

The answer was in the cookbook! What do people usually do with blockchain? They exchange tokens. The Solana team started to think about this `SystemProgram` whose job is to administer new accounts and transfer SOL between two parties. We will see later what are those "accounts" about.

So, now we understand why we need that SystemProgram when we want to send SOL.

The SNP Crew & SPL Crew
All Solana Validators Nodes are born with a set of Solana Native Programs. And the System Program is one of these native programs. They each exist with a specific job to perform. So depending on what you are trying to achieve, you would talk to different programs:

What is the difference between the native ones and the library ones?

Ok, we solved one part of the equation: the WHO! Solana needs to know which program you want to talk to when we make a transaction. In the case of sending SOL, we had to talk to the System Program. But what is a transaction anyway?

Once again, Solana cookbook to the rescue! https://solanacookbook.com/core-concepts/transactions.html#transactions

Clients can invoke programs by submitting a transaction to a cluster. [...] When a transaction is submitted, the Solana Runtime will process its instructions in order and atomically. [...]

From the definition, we learn two things:

  1. Transaction = array of instructions.
  2. Transactions are collected by the cluster and processed by the Solana Runtime.

This deserves a little drawing, 1 transaction = n instructions:

The above image in code:

// These are all of type 'TransferInstruction'
const splInstruction = splToken.Token.createTransferInstruction(...)
const systemInstruction = SystemProgram.transfer(...)
const myProgramInstruction = new TransferInstruction(...)
            
const transaction = new Transaction()
    .add(splInstruction)
    .add(systemInstruction)
    .add(myProgramInstruction)
)

In our use case, when we wanted to send SOL, we only had one instruction in our transaction:

const transaction = new Transaction().add(
    SystemProgram.transfer({
        fromPubkey: publicKey,
        toPubkey: destAddress,
        lamports: amount,
    })
);

Wait, what instruction are you talking about? The solution is... not in the cookbook! Got you! We just have to right-click and go to definition in the code directly. Code is truth, always:

Can't go to definition? Here's the code: https://github.com/solana-labs/solana/blob/master/web3.js/src/system-program.ts#L769

As we can see, SystemProgram.transfer is just a helper function that Solana devs created to craft an instruction:

static transfer(
    params: TransferParams | TransferWithSeedParams,
): TransactionInstruction {

    // ... <--- some Solana voodoo here that we haven't learned yet
  
    return new TransactionInstruction({
      ...
    });
}

Don't be impressed by the typescript code! 😉 What is interesting to us is the return type TransactionInstruction. Ok, let me save you from this mountain of code; get out of there. We will tackle this later in the chapter called The Revenge Of Instructions. 😈

Next Part:
Remember our definition? The second part was "Transactions are collected by the cluster and processed by the Solana Runtime."

No navy seal code diving this time. Let me take you on a journey:

A transaction has a long way to go before being processed by the Solana runtime, not as long as Marco Polo Travels, but it goes like this:

  1. First, we batch one or more instructions into a transaction (we just explained this before).
  2. Then, we sign that letter using the user's wallet.
  3. We post that letter through the Internet to the Solana RPC Node, a node that collects transactions.
  4. The Solana RPC Node will then dispatch this transaction to a Solana Validator Node.
  5. The Solana Runtime will then process the transaction and broadcast it to other blockchain nodes.

TODO: confirm this broadcasting part and the TPU part.

🏆 Achievement - Programs & Transactions 101

Why 101? We just uncovered the surface of transactions and programs, but as you might start to understand now, I only cover what you need at the moment.

In Learning How To Learn, Professor Barbara Oakley explained that the human brain can only hold in its Short Term Memory storage 7 items at a time! Our RAM as humans is terrible! (Well, neuroscience is as volatile as cryptocurrencies so maybe they will figure out it's actually a little less or more...)

So, it's not time for you to learn about instruction yet my Padawan. There is a time for everything, but since you are asking, here's what's in our next adventures:

  • Blockhashes.
  • How to write an Instruction.
  • Signatures.
  • Transaction Fees (you thought it was free? 😗)
  • Accounts (the dreaded one 👹).
  • The Transaction Processing Unit.

Before moving on, let's do some mnemonics:

  • "An instruction is like writing a love letter to a program (the WHO).
  • "We can send many of these love letters by batching them into a transaction" (do not try this at home).
  • "The super letter is collected by the guardian of Solana Realm; the Solana RPC."
  • "And dispatched to Solana Validators (the little bees of the blockchain)."
Who still writes letters...?

And voila! Take a little break, get a coffee, tea, or water, and enjoy the blue sky. The urge to finish what you started will take you back, and your brain will be refreshed for our very anticipated quiz!

The final code is here: https://github.com/mwrites/dapp-scaffold/commit/60d7191b7f392c9b633424d626887b185729566a

💡 Quiz

  • [ ] Why do we need to specify SystemProgram when sending SOL?
  • [ ] What is a Transaction?
  • [ ] Who owns the Solana Native Programs?
  • [ ] What is a Solana RPC Node?

🎙 Optional: Your Turn To Get The Mic!

  • [ ] Our send SOL button is always sending 1 SOL, and on top of that, it's sending to a random address. Can you add an input box to specify the receiver address and the amount?

🏆 Achievement - Transactions

The final code is here: https://github.com/mwrites/dapp-scaffold/commit/60d7191b7f392c9b633424d626887b185729566a

📙 Cookbook References


🎬  Stay Tuned For The Next Series!


Going Further

A Horror Error Story

It's very typical, and it's a full-time part of our job as devs to learn how to deal with errors. But, unfortunately, nothing ever goes smoothly when playing with cutting-edge tech and even more so in blockchains as the code evolves sooo quickly. At this point, you might have encountered two errors:

  • "Transaction simulation failed: Attempt to debit an account but found no record of a prior credit." aka "Where's the money?"
Your wallet is probably on the mainnet while the dapp is trying to work with devnet.
  • "Transaction leaves an account with a lower balance than rent-exempt minimum" -> aka "Where's the money?" Number 2.
Since May 2022, all accounts need to be rent-exempt, we haven't talked about rent yet, but basically, it means you are not sending enough SOL to cover the rent fee. How can you find out about the rent fee? Run solana rent with the Solana CLI.

Cool, but about the other errors? Where can I read about them and understand what is going on? Errors are defined here: https://github.com/solana-labs/solana-program-library/blob/master/token/program/src/error.rs.

Where To Ask Questions?

How To Stay Up To Date With Solana?


(Super Optional) Creating a New NextJS Page

Ok, you made it!! This part is optional as it is purely NextJS stuff. Let's just organize the code a bit and move the transaction button to a new page called pages/basics.tsx:

import type { NextPage } from "next";
import Head from "next/head";
import { BasicsView } from "../views";


const Basics: NextPage = (props) => {
  return (
    <div>
      <Head>
        <title>Solana Scaffold</title>
        <meta
          name="description"
          content="Basic Functionality"
        />
      </Head>
      <BasicsView />
    </div>
  );
};

export default Basics;

What is this BasicsView? It doesn't exist yet, so let's create it in views/basics/index.tsx. Inside it, is where we will display our  <SendTransaction /> component.

import { FC } from "react";
import { SendTransaction } from '../../components/SendTransaction';


export const BasicsView: FC = ({ }) => {
  return (
	<div className="md:hero mx-auto p-4">
      <div className="md:hero-content flex flex-col">
        <h1 className="text-center text-5xl font-bold text-transparent bg-clip-text bg-gradient-to-tr from-[#9945FF] to-[#14F195]">
          Basics
        </h1>
        {/* CONTENT GOES HERE */}
        <div className="text-center">
          <SendTransaction />
        </div>
      </div>
    </div>
  );
};

Let's make our life easier with imports by adding BasicsView to views/index.tsx:

export { HomeView } from "./home";
export { BasicsView } from "./basics";

Also before we forget, let's make the link to our new page appear in our components/AppBar.tsx. Open components/AppBar.tsx (I am omitting the whole HTML to ease your eyes, look for "Nav Links"):

export const AppBar: FC = props => {
  const { autoConnect, setAutoConnect } = useAutoConnect();

  return (
	...
	
        {/* Nav Links */}
        <div className="hidden md:inline md:navbar-center">
          <div className="flex items-stretch">
            <Link href="/">
              <a className="btn btn-ghost btn-sm rounded-btn">Home</a>
            </Link>
            <Link href="/basics">
              <a className="btn btn-ghost btn-sm rounded-btn">Basics</a>
            </Link>
          </div>
        </div>
    ...
    );
};

Finally, we can create components/SendTransaction.tsx:

import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { FC, useCallback } from 'react';
import { notify } from "../utils/notifications";


export const SendTransaction: FC = () => {
    const { connection } = useConnection();
    const { publicKey } = useWallet();


    const onClick = useCallback(async () => {
        console.log("Sending SOL!")

		// We need to do something like
		// sendSol(fromPublicKey, toPublicKey, amount);

    }, [publicKey, notify, connection]);


    return (
        <div>
            <button
                className="group w-60 m-2 btn animate-pulse disabled:animate-none bg-gradient-to-r from-[#9945FF] to-[#14F195] hover:from-pink-500 hover:to-yellow-500 ... "
                onClick={onClick} disabled={!publicKey}
            >
                <div className="hidden group-disabled:block ">
                    Wallet not connected
                </div>
                <span className="block group-disabled:hidden" > 
                    Send Transaction 
                </span>
            </button>
        </div>
    );
};

  • connection: we will need to communicate with Solana, so we will need to configure one.
  • publicKey: we will also need the connected user publicKey, to specify the fromPublicKey.

Result:

Share article