Skip to main content

Intro to Solana Development - Lesson 3 - SPL Token Program in Dapps

Updated on
Nov 12, 2025

15 min read

Overview

In this lesson, we will hone our knowledge and understanding of the SPL Token Program JS SDK by adding two new features to our sample dapp:

  1. An init token button that will create a new token mint and optionally mint tokens to our wallet
  2. A table that will display all of the tokens in our wallet

Ready to start coding? Open up your Next.js project from lesson 2. We will pick up where we left off. Alternatively you can fork the Demo Progress Repository and start from there.

Set Up Your Project

Install SPL-Token Dependencies

From your project's root directory, run the following command to install the @solana/spl-token library:

yarn add @solana/spl-token
#or
npm install @solana/spl-token

Create SPL Token Utils

Before building our react components, let's create a new utils file containing the functions we will need to interact with the SPL Token Program. Inside of your utils folder, create a new file called token-lib.ts:

echo > utils/token-lib.ts

We will use this file to create functions to help us interact with the SPL Token Program. Let's start by importing the following libraries:

import { Connection, Keypair, PublicKey, SystemProgram, TransactionInstruction } from "@solana/web3.js";
import {
getMinimumBalanceForRentExemptMint,
getAssociatedTokenAddressSync,
MINT_SIZE, TOKEN_PROGRAM_ID,
createInitializeMintInstruction,
createMintToInstruction,
createAssociatedTokenAccountIdempotentInstruction,
} from "@solana/spl-token";

Everything here should look familiar to the previous lesson--we will be building something very similar. Let's define a helper function and some interfaces before we create our function. Add the following code to the file:

const amountToDecimalAmount = (amount: number, decimals: number) => {
return amount * (10 ** decimals);
}

interface createNewTokenParams {
authority: PublicKey,
connection: Connection,
numDecimals?: number,
numTokens?: number
}

interface createNewTokenReturn {
instructions: TransactionInstruction[],
signers: Keypair[]
}


export async function createNewToken({
authority,
connection,
numDecimals = 2,
numTokens = 100
}: createNewTokenParams): Promise<createNewTokenReturn> {

}

Here we are defining a helper function, amountToDecimalAmount. This function will take in an amount and the number of decimals and return the amount in decimal form. For example, if we have two decimals and an amount of 1, the function will return 100. If we have three decimals and an amount of 1, the function will return 1,000 (the value expected by the SPL Token Program).

We are also defining two interfaces. The first interface, createNewTokenParams, represents the parameters our function will take in (effectively the same parameters we used in the last example). The second interface, createNewTokenReturn, defines the return value of our function. If you recall, in our previous lesson on the wallet adapter, we created a SendTransactionTemplate, which accepts an array of instructions and an optional array of signers. Our return statement includes both values so that we can easily use them to create a transaction button from our template. Note that we have framed our function as an async function. This is because we will use the getMinimumBalanceForRentExemptMint function, an async function. We are also defining a couple of default values for the function so that the function will still work if we don't pass in any values.

Now that we have defined our helper function and interfaces, let's build our createNewToken function. Add the following code to the createNewToken function:

export async function createNewToken({
authority,
connection,
numDecimals = 0,
numTokens = 0
}: createNewTokenParams): Promise<createNewTokenReturn> {
const instructions: TransactionInstruction[] = [];
// Instruction 1 - Create a new account
const requiredBalance = await getMinimumBalanceForRentExemptMint(connection);
const mintKeypair = Keypair.generate();
const ix1 = SystemProgram.createAccount({
fromPubkey: authority,
newAccountPubkey: mintKeypair.publicKey,
space: MINT_SIZE,
lamports: requiredBalance,
programId: TOKEN_PROGRAM_ID,
});

// Instruction 2 - Initialize the new account as a mint
const ix2 = createInitializeMintInstruction(
mintKeypair.publicKey,
numDecimals,
authority,
authority
);
instructions.push(ix1, ix2)

// If no tokens are to be minted, return the init instructions without appending any minting instructions
if (numTokens === 0) return { instructions, signers: [mintKeypair] };

// Instruction 3 - Create an associated token account for the mint
const tokenATA = getAssociatedTokenAddressSync(mintKeypair.publicKey, authority);
const ix3 = createAssociatedTokenAccountIdempotentInstruction(
authority,
tokenATA,
authority,
mintKeypair.publicKey
);

// Instruction 4 - Mint tokens to the new account
const ix4 = createMintToInstruction(
mintKeypair.publicKey,
tokenATA,
authority,
amountToDecimalAmount(numTokens, numDecimals)
);
instructions.push(ix3, ix4)

return { instructions: instructions, signers: [mintKeypair] };
}

This should look very familiar.

  • First, we create an instruction for a new account for the mint using the SystemProgram.createAccount function.
  • Next, we create an instruction for initializing the new account as a mint using the createInitializeMintInstruction function.
  • We check to see if we need to mint any tokens. If we don't, we return the instructions and signers (remember we need to sign the transaction with the mint's keypair to initialize it as a new account).
  • If we do need to mint tokens, we create an instruction for creating an associated token account using the createAssociatedTokenAccountIdempotentInstruction function and another for minting tokens to the new account using the createMintToInstruction function.
  • Finally, we return all of the instructions as an array, instructions and the mintKeypair as an array as well.

Great job. Because we constructed a return statement that matches the SendTransactionTemplate interface, we can use this function to create a transaction button in our Dapp. Let's do that now.

Create a New Token Button

Let's create a new component that will allow us to create a new token. Create a new file in your components/SendTransactionButtons folder called CreateNewTokenButton.tsx:

echo > components/SendTransactionButtons/CreateToken.tsx

Open the file and add the following code:

import { TransactionInstruction, PublicKey, Keypair } from "@solana/web3.js";
import { SendTransactionTemplate } from "./SendTransactionTemplate";
import { createNewToken } from "../../utils/token-lib";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { useEffect, useState } from "react";

interface CreateTokenButtonProps {
numDecimals?: number;
numTokens?: number;
}

const CreateTokenButton = ({ numDecimals, numTokens }: CreateTokenButtonProps) => {
const [instructions, setInstructions] = useState<TransactionInstruction[]>([]);
const [extraSigners, setExtraSigners] = useState<Keypair[]>([]);

// TODO - Add your code here

return (
<SendTransactionTemplate
transactionInstructions={instructions}
buttonLabel={`Init New Token`}
extraSigners={extraSigners}
/>
);
}

export default CreateTokenButton;

Here's all we are doing:

  • Importing a few libraries we will need, including our createNewToken function and the SendTransactionTemplate component we created in the previous lesson.
  • Defining the props for our component, which will allow us to use the component declaration for determining the number of decimals for our token and the number of tokens to mint (these can be optional since we provided default values in our createNewToken function).
  • Defining a couple of state variables that we will use to store the instructions and signers for our transaction.
  • We then pass these state variables into our SendTransactionTemplate component. This will allow us to create a transaction button from our template.

All that is missing is the code that will create the instructions and signers for our transaction. Since our createNewToken function returns a promise for instructions and extra signers, we need to call it and then use the returned values to set our state variables. Let's add that now. Add the following code to the CreateTokenButton component below the state setters and before the return statement:

    const { publicKey: authority } = useWallet();
const { connection } = useConnection();

useEffect(() => {
if (!authority) return;
createNewToken({ authority, connection, numDecimals, numTokens })
.then(({ instructions, signers }) => {
setInstructions(instructions);
setExtraSigners(signers);
})
.catch((err) => {
console.log(err);
});
}, [authority, connection]);

First, we are using the useWallet() and useConnection() hooks to get our connected wallet public key (which we are calling authority) and the connection to the Solana cluster. Remember, since we wrapped our app in SolanaProviders, these hooks are available to us anywhere in our app. We then use the useEffect hook to call our createNewToken function and set the state variables when the authority or connection changes.

Nice job! We now have a component that will create a new token when we click the button. Let's add it to our app. Open your components/Main.tsx. First, import the CreateTokenButton component:

import CreateTokenButton from './SendTransactionButtons/CreateToken';

Then add the component to the Main component below the SendMemoButton component:

                    <SendMemoButton message='Hello from Quicknode!' />
<CreateTokenButton />

You could optionally include the numDecimals and numTokens props to customize the number of decimals and number of tokens to mint. For now, we will leave them out to use the default values.

That's it! Your app should now have a button that will create a new token and mint you some! Try it out:

npm run dev

You should see your Init New Token button. Ensure your wallet is set to Devnet and has some SOL, and then give it a shot! You should see something like this:

Init New Token

Great job! You now have a button that will create a new token and mint you some! Let's add a table that will display all of the tokens in your wallet.

Display Tokens in Your Wallet

Let's create a new component to display all the tokens in your wallet. Create a new file in your components folder called TokenTable.tsx:

echo > components/TokenTable.tsx

There is only a little Solana interaction to this part, so we are going to share the entire code and then walk through it. Open the file and add the following code:

import { shortenHash } from "@/utils/utils";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { useEffect, useState } from "react";

interface TokenRow {
mint: string;
tokenAccountAddress: string;
tokenAmount: number;
}

const TokenTable = () => {
const [rows, setRows] = useState<TokenRow[]>([]);
const { publicKey } = useWallet();
const { connection } = useConnection();

useEffect(() => {
if (!publicKey) return;
connection.getParsedTokenAccountsByOwner(publicKey, { programId: TOKEN_PROGRAM_ID })
.then(({ value }) => {
const tokens: TokenRow[] = value.map((account) => {
return {
mint: account.account.data.parsed.info.mint,
tokenAccountAddress: account.pubkey.toBase58(),
tokenAmount: account.account.data.parsed.info.tokenAmount.uiAmount,
}
})
setRows(tokens);
})
.catch((err) => {
console.log(err);
});
}, [publicKey, connection]);

return (
<div className="overflow-x-auto mt-4 text-center">
<table className="min-w-full text-white table-auto">
<thead>
<tr className="border-b border-gray-700">
<th className="px-4 py-2">Mint</th>
<th className="px-4 py-2">Token Account<br/>Address</th>
<th className="px-4 py-2">Amount</th>
</tr>
</thead>
<tbody>
{rows.map((row, index) => (
<tr key={index} className={`bg-gray-800 border-b border-gray-700 ${index % 2 === 0 ? 'bg-opacity-50' : ''}`}>
<td className="px-4 py-2">{shortenHash(row.mint)}</td>
<td className="px-4 py-2">{shortenHash(row.tokenAccountAddress)}</td>
<td className="px-4 py-2">{row.tokenAmount}</td>
</tr>
))}
</tbody>
</table>
{rows.length === 0 && (
<div className="text-gray-500 mt-4">No tokens found.</div>
)}
</div>
);

}

export default TokenTable;

In short, here's what we are doing: we use a Solana method on the Connection class called getParsedTokenAccountsByOwner to get our wallet's token accounts. We then use the useEffect hook to call this method when our connected wallet or network connection changes. We store the token accounts to state and use a .map function to create a table row for each token account. Here's a more detailed breakdown:

  • First, we import a few libraries we will need.
  • Next, we define an interface for our table rows. This will allow us to use the TokenRow type to define our state variable and the .map function.
  • We then define our TokenTable component.
  • We define a state variable, rows, to store our token accounts.
  • We use the useWallet() and useConnection() hooks to get our connected wallet public key and the connection to the Solana cluster.
  • We use the useEffect hook to call the getParsedTokenAccountsByOwner method when the publicKey or connection changes. This method returns a promise for an array of token accounts. We use the .then function to map the array of token account data to an array of TokenRow objects. We then set the rows state variable to this array.
  • We then return a table that will display the token accounts. We use the .map function to create a table row for each token account. We also use the shortenHash utility function to shorten the token account and mint addresses.
  • If no token accounts are found, we display a message saying so.

That's it 🙌 Let's add this component to our app. Open your components/Main.tsx. First, import the TokenTable component:

import TokenTable from './TokenTable';

Then add it to the Main component below the CreateTokenButton component:

                    <CreateTokenButton />
<TokenTable />

Now rerun your app:

npm run dev

Do you see your tokens?

Token Table

Great work! You now have a table that will display all of the tokens in your wallet.

Continued Practice

You have done a ton of great work here, so feel free to call it a day. If you are looking for more practice, here are a few ideas:

  • Add a new column to your table that includes a button to burn one token of that token mint
  • Add a refresh button to your table (or add a refreshTokens Context to your SolanaProviders component) that will refresh the token accounts after you create a new token
  • Create some filters for your table that will allow you to filter by mint address, token account address, or token amount

Next Steps and Wrap Up

Awesome! You are well on your way to becoming a Token Program master! Though the Token program is quite powerful and an integral part of building on Solana, being able to create your own programs can be even more empowering. The next lesson will teach us how to develop programs using Anchor.


Sign in to track your course progress and interact with quizzes
Create a free Quicknode account to save your progress, complete knowledge checks, and mark lessons complete.
Create AccountSign In

Remember to save your project, as we will use it again later in this course.