SOLANA QUEST

Challenge #3

Introduction To Spl Tokens


artwork1-01.png

This guide will teach you how to build and deploy the Solana program and connect to the UI for a basic SPl token mint and transfer dApp. This d-App will allow you to mint and transfer SPL tokens on Solana.

What you will learn

Prerequisites

For this guide, you will need to have your local development environment setup with a few tools:

What we are building

We are developing an SPL token mint and Transfer program that creates spl token mint and transfers tokens to another user. In this dapp, we will utilize both types of tokens fungible and NFT on the Solana DevNet network.

spllandingpage Live At: spldapp_live

Setting up the project

Clone this repository into your local system

git clone git@github.com:solana-based-quests/S-Sol-tokens.git

Now change the directory to

cd s-sol-tokens

Then head over to the client folder to run this d-app

cd solana-client

Update project dependencies

npm install

And Run

npm run dev

Dapp accounts overview

splAccounts

In the above diagram, the user's wallet is a signer that will pay for all account creations. The mint account includes the decimal, mint authority and freezes authority over tokens. The system program facilitates the creation of the Mint account. The token account is used for creating an Associated token account for holding SPL tokens. All other Solana ecosystem accounts facilitate the minting and transferring operation of SPL Token.

Anchor program development

If you're new to Anchor, The Anchor Book and Anchor Examples are great references to help you learn.

In s-sol-tokens, navigate to spltokens-sol-program/programs/src/lib.rs.

use anchor_lang::prelude::*;
pub mod instructions;
pub use instructions::*;
declare_id!("C2eEY8eeediwD2YXvZZGQ74G9kPB5PeRNFGUzSiFadcW");

#[program]
pub mod spltokens {
    use super::*;

    pub fn mint_nft(ctx: Context<MintNFT>, name: String, symbol: String, uri: String) -> Result<()> 
    {
        mint_nft::mint_nft(ctx, name, symbol, uri)
    }

    pub fn mint_token(ctx: Context<MintToken>, _decimals:u8, name: String, symbol: String, uri: String, amount: u64) -> Result<()>
    {
        mint_token::mint_token(ctx, _decimals, name, symbol, uri, amount)
    }

    pub fn transfer_tokens(ctx: Context<TransferToken>, amount: u64) -> Result<()> 
    {
        transfer_token::transfer_tokens(ctx, amount)
    }
}

In the code above:

Create Mint NFT instruction

In s-sol-tokens, navigate to spltokens-sol-program/programs/src/instrcutions/mint_nft.rs.

use anchor_lang::prelude::*;
use anchor_spl::{ associated_token::AssociatedToken, metadata::
    {create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3, CreateMetadataAccountsV3, Metadata}, token::{mint_to, Mint, MintTo, Token, TokenAccount }};
use mpl_token_metadata::{ pda::{ find_master_edition_account, find_metadata_account}, state::DataV2 };

pub fn mint_nft(ctx: Context<MintNFT>, name: String, symbol: String, uri: String) -> Result<()> 
{
    let cpi_context = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        MintTo {
            mint: ctx.accounts.mint.to_account_info(),
            to: ctx.accounts.associated_token_account.to_account_info(),
            authority: ctx.accounts.signer.to_account_info(),
        },
    );

    mint_to(cpi_context, 1)?;

    let cpi_context = CpiContext::new(
        ctx.accounts.token_metadata_program.to_account_info(),
        CreateMetadataAccountsV3 {
            metadata: ctx.accounts.metadata_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            mint_authority: ctx.accounts.signer.to_account_info(),
            update_authority: ctx.accounts.signer.to_account_info(),
            payer: ctx.accounts.signer.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        },
    );

    let data_v2 = DataV2 {
        name,
        symbol,
        uri,
        seller_fee_basis_points: 0,
        creators: None,
        collection: None,
        uses: None,
    };

    create_metadata_accounts_v3(cpi_context, data_v2, false, true, None)?;

    let cpi_context = CpiContext::new(
        ctx.accounts.token_metadata_program.to_account_info(),
        CreateMasterEditionV3 {
            edition: ctx.accounts.master_edition_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            update_authority: ctx.accounts.signer.to_account_info(),
            mint_authority: ctx.accounts.signer.to_account_info(),
            payer: ctx.accounts.signer.to_account_info(),
            metadata: ctx.accounts.metadata_account.to_account_info(),
            token_program: ctx.accounts.token_program.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        },
    );

    create_master_edition_v3(cpi_context, None)?;

    Ok(())
}

#[derive(Accounts)]
pub struct MintNFT<'info> 
{
    /// CHECK: signer check
    #[account(mut, signer)]
    signer: AccountInfo<'info>,

    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key()
    )]
    mint: Account<'info, Mint>,

    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer
    )]
    pub associated_token_account: Account<'info, TokenAccount>,

    /// CHECK:
    #[account(mut, address = find_metadata_account(&mint.key()).0)]
    pub metadata_account: AccountInfo<'info>,

    /// CHECK:
    #[account(mut, address = find_master_edition_account(&mint.key()).0)]
    pub master_edition_account: AccountInfo<'info>,

    pub token_program: Program<'info, Token>,
    pub rent: Sysvar<'info, Rent>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub token_metadata_program: Program<'info, Metadata>,
    pub system_program: Program<'info, System>,
}

let’s break down to see what is happening here
In the above code,

Imports-

We imports anchor_lang, anchor_spland mpl_token_metadata.

Instruction handler logic-

In The mint_nft function

    CreateMetadataAccountsV3 {
            metadata: ctx.accounts.metadata_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            mint_authority: ctx.accounts.signer.to_account_info(),
            update_authority: ctx.accounts.signer.to_account_info(),
            payer: ctx.accounts.signer.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        },
 let data_v2 = DataV2 {
        name,
        symbol,
        uri,
        seller_fee_basis_points: 0,
        creators: None,
        collection: None,
        uses: None,
    };
    CreateMasterEditionV3 {
            edition: ctx.accounts.master_edition_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            update_authority: ctx.accounts.signer.to_account_info(),
            mint_authority: ctx.accounts.signer.to_account_info(),
            payer: ctx.accounts.signer.to_account_info(),
            metadata: ctx.accounts.metadata_account.to_account_info(),
            token_program: ctx.accounts.token_program.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        },

Account creation and validation-

In Anchor Various types of constraints can be applied using the #[account(..)] attribute.

In the signer Account

In the Mint account, The init constraint creates the account through a Cross-Program Invocation (CPI) to the system program and initializes it by setting its account discriminator. Additionally, it applies specific constraints:

In the associated_token_account, The init_if_needed has the same functionality as the init. However, it only runs if the account does not exist yet. Additionally, it applies specific constraints:

Other ecosystem accounts for handling this instruction logic:

metadata_account: is responsible for setting up metadata for the token. master_edition_account: is responsible for setting up an edition for the NFT. token_program: is a type of Token
rent: is a type of Rent
associated_token_program: is a type of AssociatedToken
token_metadata_program: is a type of Metadata
system_program: is a type of System

Now, At this stage, we created three cpi calls for:

Create Fungible Mint instruction

In s-sol-tokens, navigate to spltokens-sol-program/programs/src/instrcutions/mint_token.rs.

use anchor_lang::prelude::*;
use anchor_spl::{ associated_token::AssociatedToken, metadata::{ create_metadata_accounts_v3, CreateMetadataAccountsV3 }, token::{mint_to, Mint, MintTo, Token, TokenAccount }};
use mpl_token_metadata::{ pda::find_metadata_account, state::DataV2 };

pub fn mint_token(ctx: Context<MintToken>, _decimals: u8, name: String, symbol: String, uri: String, amount: u64) -> Result<()> 
{
    let cpi_context = CpiContext::new(
        ctx.accounts.token_program.to_account_info(),
        MintTo {
            mint: ctx.accounts.mint.to_account_info(),
            to: ctx.accounts.associated_token_account.to_account_info(),
            authority: ctx.accounts.signer.to_account_info(),
        },
    );

    mint_to(cpi_context, amount)?;

    let cpi_context = CpiContext::new(
        ctx.accounts.token_metadata_program.to_account_info(),
        CreateMetadataAccountsV3 {
            metadata: ctx.accounts.metadata_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            mint_authority: ctx.accounts.signer.to_account_info(),
            update_authority: ctx.accounts.signer.to_account_info(),
            payer: ctx.accounts.signer.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        },
    );

    let data_v2 = DataV2 {
        name,
        symbol,
        uri,
        seller_fee_basis_points: 0,
        creators: None,
        collection: None,
        uses: None,
    };

    create_metadata_accounts_v3(cpi_context, data_v2, false, true, None)?;

    Ok(())
}

#[derive(Accounts)]
#[instruction(decimals: u8)]
pub struct MintToken<'info> 
{
    /// CHECK: signer check
    #[account(mut, signer)]
    signer: AccountInfo<'info>,

    #[account(
        init,
        payer = signer,
        mint::decimals = decimals,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    mint: Account<'info, Mint>,

    /// CHECK:
    #[account(mut, address = find_metadata_account(&mint.key()).0)]
    pub metadata_account: AccountInfo<'info>,

    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer,
    )]
    pub associated_token_account: Account<'info, TokenAccount>,

    /// CHECK: account constraint checked in account trait
    #[account(address = mpl_token_metadata::id())]
    pub token_metadata_program: UncheckedAccount<'info>,

    pub token_program: Program<'info, Token>,
    pub rent: Sysvar<'info, Rent>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub system_program: Program<'info, System>
}

let’s break down to see what is happening here
In the above code,

Imports-

we imports anchor_lang, anchor_spland mpl_token_metadata.

Instruction handler logic-

The mint_token function

     CreateMetadataAccountsV3 {
            metadata: ctx.accounts.metadata_account.to_account_info(),
            mint: ctx.accounts.mint.to_account_info(),
            mint_authority: ctx.accounts.signer.to_account_info(),
            update_authority: ctx.accounts.signer.to_account_info(),
            payer: ctx.accounts.signer.to_account_info(),
            system_program: ctx.accounts.system_program.to_account_info(),
            rent: ctx.accounts.rent.to_account_info(),
        },
 let data_v2 = DataV2 {
        name,
        symbol,
        uri,
        seller_fee_basis_points: 0,
        creators: None,
        collection: None,
        uses: None,
    };

Account creation and validation-

In the Mint account, The init constraint creates the account through a Cross-Program Invocation(CPI) to the system program and initializes it by setting its account discriminator. Additionally, it applies specific constraints:

In the associated_token_account, The init_if_needed has the same functionality as the init. However, it only runs if the account does not exist yet. Additionally, it applies specific constraints:

Other ecosystem accounts for handling this instruction logic:

token_metadata_program: is responsible for setting up metadata for the token.
token_program: is a type of Token
rent: is a type of Rent
associated_token_program: is a type of AssociatedToken
system_program: is a type of System

Now, At this stage, we created two cpi calls for:

Create SPL Token Transfer instruction

use anchor_lang::prelude::*;
use anchor_spl::{ token::{self, Transfer}, token::{ Token, TokenAccount }};

pub fn transfer_tokens(ctx: Context<TransferToken>, amount: u64) -> Result<()> 
{
    let cpi_accounts = Transfer 
    {
        from: ctx.accounts.from_ata.to_account_info(),
        to: ctx.accounts.to_ata.to_account_info(),
        authority: ctx.accounts.from.to_account_info(),
    };
    let cpi_program = ctx.accounts.token_program.to_account_info();
    
    token::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)?;

    Ok(())
}

#[derive(Accounts)]
pub struct TransferToken<'info> 
{
    pub from: Signer<'info>,

    #[account(mut)]
    pub from_ata: Account<'info, TokenAccount>,

    #[account(mut)]
    pub to_ata: Account<'info, TokenAccount>,

    pub token_program: Program<'info, Token>
}

Imports-

we imports anchor_lang, anchor_spl.

Instruction handler logic-

In The transfer_tokens function

Account creation and validation-

At this stage, we created one cpi call to transfer spl tokens into the user’s associated token accounts.

Build and deploy Anchor program

In s-sol-tokens, navigate to spltokens-sol-program and run

anchor build

This will build your anchor program and generate a target folder(we will use generated IDL and types for the Solana program to connect our UI to this program).

To deploy this program run

anchor deploy

After this, the program will deployed to an address. Pick this program address and update this address to the following files.

At this stage, we are done with the SPL token Mint and Transfer Solana program development.

Connecting Solana program to UI

Solana-app-scaffold already sets up a UI with prebuilt Hooks and a wallet connector for you. All you need to do is simply modify it to fit your newly created program.

Since our SPl token mint and transfer has four instructions, we will need components in the UI that will be able to call each of these instructions:

In your project folder open s-sol-tokens/solana-client and add the generated IDL file at the root level from s-sol-tokens/spltokens-sol-program/target/idl/spltokens.json

let’s head over to call your instructions from the client side.

Create NFT Mint

In your project folder open s-sol-tokens/solana-client/src/components/MintNFTButton.tsx

Here is how you get your programID from IDL.

const programId = new PublicKey(idl.metadata.address);

you can generate a program API to call each instruction in the program

 const getProgram = () => {
        /* create the provider and return it to the caller */

        const provider = new AnchorProvider(connection, wallet as any, opts);
        /* create the program interface combining the idl, program ID, and provider */
        const program = new Program(idl as Idl, programId, provider);
        return program;
    };

  const program = getProgram();

Set up the umi client and fetch metadata and master edition PDA account using the mint account.

 const umi = createUmi("https://api.devnet.solana.com").use(walletAdapterIdentity(wallet)).use(mplTokenMetadata());
    //const mint = anchor.web3.Keypair.generate();

    const associatedTokenAccount = wallet.publicKey !== null && getAssociatedTokenAddressSync(
        mintKeypair.publicKey,
        wallet.publicKey
    );

    let metadataAccount = findMetadataPda(umi, {
        mint: publicKey(mintKeypair.publicKey),
    })[0];

    let masterEditionAccount = findMasterEditionPda(umi, {
        mint: publicKey(mintKeypair.publicKey),
    })[0];

    const MetaData = {
        name: "oggggg",
        symbol: "Oggy",
        uri: "https://raw.githubusercontent.com/solana-developers/program-examples/new-examples/tokens/tokens/.assets/nft.json",
    };

Call mintNft fn to mint NFT into Minter’s wallet.

 const mintNft = async (e) => {
        setLoading(true);
        try {
            e.preventDefault();
            const metaplex = Metaplex.make(connection);

            const tx = await program.methods
                .mintNft(MetaData.name, MetaData.symbol, MetaData.uri)
                .accounts({
                    signer: wallet.publicKey,
                    mint: mintKeypair.publicKey,
                    associatedTokenAccount,
                    metadataAccount,
                    masterEditionAccount,
                    tokenProgram: TOKEN_PROGRAM_ID,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID,
                    systemProgram: anchor.web3.SystemProgram.programId,
                    rent: anchor.web3.SYSVAR_RENT_PUBKEY,
                })
                .signers([mintKeypair])
                .rpc();

            console.log(`mint nft tx: https://explorer.solana.com/tx/${tx}?cluster=devnet`);
            console.log(`minted nft: https://explorer.solana.com/address/${mintKeypair.publicKey}?cluster=devnet`);

            setTxSig(tx);

            let metadata = await metaplex
                .nfts()
                .findByMint({ mintAddress: mintKeypair.publicKey, tokenOwner: wallet.publicKey });
            setNftDetails(metadata);
            notify({ message: "NFT Minted" });
        } catch (err) {
            console.log(err);
            notify({ message: err.message });
        }
        setLoading(false);
    };

Transfer NFT to another user

In your project folder open s-sol-tokens/solana-client/src/components/nft/NFTCard.tsx and call the transferNFT fn to transfer NFT to buyer’s associated token account.

 const transferNFT = async () => {
        setLoading(true);
        try {
            const transaction = new Transaction();

            const sellerTokenAccount = await getAssociatedTokenAddress(
                mint.publicKey,
                publicKey,
                false,
                TOKEN_PROGRAM_ID,
                ASSOCIATED_TOKEN_PROGRAM_ID
            );

            const buyerPublicKey = new anchor.web3.PublicKey(buyer);

            // Derive wallet's associated token account address for mint
            const buyerTokenAccount = await getAssociatedTokenAddress(
                mint.publicKey,
                buyerPublicKey,
                false,
                TOKEN_PROGRAM_ID,
                ASSOCIATED_TOKEN_PROGRAM_ID
            );

            try {
                await getAccount(connection, buyerTokenAccount);
            } catch (e) {
                transaction.add(
                    createAssociatedTokenAccountInstruction(
                        publicKey,
                        buyerTokenAccount,
                        buyerPublicKey,
                        mint.publicKey,
                        TOKEN_PROGRAM_ID,
                        ASSOCIATED_TOKEN_PROGRAM_ID
                    )
                );
                transaction.feePayer = wallet.publicKey;
                const tx1 = await sendTransaction(transaction, connection);
                console.log("tx1", tx1)
            }

            const tx = await program.methods
                .transferTokens(new BN(1))
                .accounts({
                    from: publicKey,
                    fromAta: sellerTokenAccount,
                    toAta: buyerTokenAccount,
                    tokenProgram: TOKEN_PROGRAM_ID
                })
                .rpc({ skipPreflight: true });

            setTxSig(tx);
            notify({ message: "NFT Transferred" });
        } catch (err) {
            console.log(err);
            notify({ message: err.message });
        }
        setLoading(false);
    };

Create Fungible Token Mint

In your project folder open s-sol-tokens/solana-client/src/components/fungible/FungibleTokenMint.tsx and call the createMint fn to create a fungible token mint and mint tokens into minter’s wallet.

  const createMint = async (event) => {
        event.preventDefault();

        setLoading(true);

        if (!publicKey) {
            notify({ type: "error", message: `Wallet not connected!` });
            console.log("error", `Send Transaction: Wallet not connected!`);
            return;
        }
        // creating metadata address
        const metaplex = Metaplex.make(connection);
        const metadataAddress = await metaplex.nfts().pdas().metadata({ mint: mintKeypair.publicKey });

        const associatedTokenAccount = wallet.publicKey !== null && getAssociatedTokenAddressSync(
            mintKeypair.publicKey,
            wallet.publicKey
        );

        // create mint transaction
        try {
            const createMintTransaction = await program.methods
                .mintToken(
                    3, // 0 decimals for NFT
                    tokenTitle, // NFT name
                    tokenSymbol, // NFT symbol
                    tokenUri, // NFT URI
                    new anchor.BN(9999999999999)
                )
                .accounts({
                    signer: wallet.publicKey,
                    mint: mintKeypair.publicKey,
                    associatedTokenAccount,
                    metadataAccount: metadataAddress,
                    tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID,
                    tokenProgram: TOKEN_PROGRAM_ID,
                    rent: anchor.web3.SYSVAR_RENT_PUBKEY,
                    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
                    systemProgram: anchor.web3.SystemProgram.programId,
                })
                .signers([mintKeypair])
                .rpc();
            console.log("Your transaction signature", createMintTransaction);
            let mintAccount = await getMint(connection, mintKeypair.publicKey);
            console.info("mintAccount", mintAccount.address.toString());

            setMint(mintAccount.address.toString());
            setTxSig(createMintTransaction);
        } catch (error) {
            notify({ type: "error", message: `Transaction failed!`, description: error?.message });
            console.log("error", `Transaction failed! ${error?.message}`);
            return;
        }

        setLoading(false);
    };

Transfer Fungible Token to another user

In your project folder open s-sol-tokens/solana-client/src/components/fungible/TransfertoOtherWallet.tsx and call the transferfungible fn to transfer fungible tokens to the receiver’s Associated token account.

const transferfungible = async (event) => {
        event.preventDefault();
        setLoading(true);
        if (!connection || !publicKey) {
            return;
        }
        const transaction = new web3.Transaction();

        const mintPubKey = new web3.PublicKey(event.target.mint.value);
        const recipientPubKey = new web3.PublicKey(event.target.recipient.value);
        let amount = event.target.amount.value;
        amount = amount * 10 ** 9;

        const senderAta = await getAssociatedTokenAddress(
            mintPubKey,
            publicKey,
            false,
            TOKEN_PROGRAM_ID,
            ASSOCIATED_TOKEN_PROGRAM_ID
        );

        const receiverAta = await getAssociatedTokenAddress(
            mintPubKey,
            recipientPubKey,
            false,
            TOKEN_PROGRAM_ID,
            ASSOCIATED_TOKEN_PROGRAM_ID
        );

        try {
            await getAccount(connection, receiverAta);
        } catch (e) {
            transaction.add(
                createAssociatedTokenAccountInstruction(
                    publicKey,
                    receiverAta,
                    recipientPubKey,
                    mintPubKey,
                    TOKEN_PROGRAM_ID,
                    ASSOCIATED_TOKEN_PROGRAM_ID
                )
            );
        }

        amount = new anchor.BN(amount);

        const transferTx = await program.methods
            .transferTokens(amount)
            .accounts({
                from: publicKey,
                fromAta: senderAta,
                toAta: receiverAta,
                tokenProgram: TOKEN_PROGRAM_ID
            })
            .instruction();

        transaction.add(transferTx);
        const signature = await sendTransaction(transaction, connection);
        await connection.confirmTransaction(signature, "confirmed");

        setTxSig(signature);
        setTokenAccount(receiverAta.toString());

        const account = await getAccount(connection, receiverAta);
        setBalance(account.amount.toString());
        setLoading(false);
    };

Resources-:
github : spldapp_github
vercel : spldapp_vercel