How to mint NFTs on Solana using Rust and Metaplex

solana nfts

Welcome readers. This is the start of a new series of blog posts about Solana development and in this one, you’ll learn how to write a custom contract to mint your NFT in only 4 steps.

In Solana development, you’ll face a lot of weird custom errors and bugs and it can be quite hard and frustrating to fix them since the Solana dev ecosystem is not as big as the Eth dev ecosystem but not to worry. When you get stuck you simply have to look in the right place for the solution.

During my development, I was constantly asking my doubts in the Anchor discord server and the Metaplex and Superteam server and looking through other code repos on GitHub and the Metaplex Program library itself.

The tools we’ll be using for this:

Use the CLI to set your network to devnet

solana config set --url devnet

To confirm if it worked check the output after entering the cmd

Config File: /Users/anoushkkharangate/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/anoushkkharangate/.config/solana/id.json
Commitment: confirmed

Next, if you haven’t already set up a file system wallet using this guide β†’ Solana wallet docs and also add some devnet sol using the command solana airdrop 1

Lastly use anchor cli to make an anchor project

anchor init <name-of-your-project>

Make sure that Anchor.toml is also set to devnet.

[features]
seeds = false
[programs.devnet]
metaplex_anchor_nft = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
[registry]
url = "https://anchor.projectserum.com"
[provider]
cluster = "devnet"
wallet = "/Users/<user-name>/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

That’s it now. You are all set to eat glass!

In your project there must be folder called programs, go to programs/<your-project-name>/Cargo.toml and add these dependencies. Make sure to use version 0.22.1 and you can use avm to change it

[dependencies]
anchor-lang = "0.22.1"
anchor-spl = "0.22.1"
mpl-token-metadata = {version = "1.2.5", features = ["no-entrypoint"]}

πŸ’« Don’t use the latest version of anchor because the anchor holds off updating itself with the latest version of solana-program it might cause conflicts between dependency requirements of the mpl crate and the anchor crates

Then go to the lib.rs file in src and import these:

use anchor_lang::prelude::*;
use anchor_lang::solana_program::program::invoke;
use anchor_spl::token;
use anchor_spl::token::{MintTo, Token};
use mpl_token_metadata::instruction::{create_master_edition_v3, create_metadata_accounts_v2};

Cool, now we can write the mint function!

First, let’s create the accounts struct for mint function

#[derive(Accounts)]
pub struct MintNFT<'info> {
#[account(mut)]
pub mint_authority: Signer<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub mint: UncheckedAccount<'info>,
// #[account(mut)]
pub token_program: Program<'info, Token>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub metadata: UncheckedAccount<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub token_account: UncheckedAccount<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
pub token_metadata_program: UncheckedAccount<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub payer: AccountInfo<'info>,
pub system_program: Program<'info, System>,
/// CHECK: This is not dangerous because we don't read or write from this account
pub rent: AccountInfo<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub master_edition: UncheckedAccount<'info>,
}

Don’t worry about the Unchecked accounts as we will pass these into the Metaplex program which will check them for us.

In order to use Unchecked accounts in Anchor you need to add this comment above each account

/// CHECK: This is not dangerous because we don't read or write from this account

Let’s make a function that uses the struct we just made to mint the token

pub fn mint_nft(
ctx: Context<MintNFT>,
creator_key: Pubkey,
uri: String,
title: String,
) -> Result<()> {
msg!("Initializing Mint NFT");
let cpi_accounts = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.payer.to_account_info(),
};
msg!("CPI Accounts Assigned");
let cpi_program = ctx.accounts.token_program.to_account_info();
msg!("CPI Program Assigned");
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
msg!("CPI Context Assigned");
token::mint_to(cpi_ctx, 1)?;
msg!("Token Minted !!!");
let account_info = vec![
ctx.accounts.metadata.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.token_metadata_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.rent.to_account_info(),
];
msg!("Account Info Assigned");
let creator = vec![
mpl_token_metadata::state::Creator {
address: creator_key,
verified: false,
share: 100,
},
mpl_token_metadata::state::Creator {
address: ctx.accounts.mint_authority.key(),
verified: false,
share: 0,
},
];
msg!("Creator Assigned");
let symbol = std::string::ToString::to_string("symb");
invoke(
&create_metadata_accounts_v2(
ctx.accounts.token_metadata_program.key(),
ctx.accounts.metadata.key(),
ctx.accounts.mint.key(),
ctx.accounts.mint_authority.key(),
ctx.accounts.payer.key(),
ctx.accounts.payer.key(),
title,
symbol,
uri,
Some(creator),
1,
true,
false,
None,
None,
),
account_info.as_slice(),
)?;
msg!("Metadata Account Created !!!");
let master_edition_infos = vec![
ctx.accounts.master_edition.to_account_info(),
ctx.accounts.mint.to_account_info(),
ctx.accounts.mint_authority.to_account_info(),
ctx.accounts.payer.to_account_info(),
ctx.accounts.metadata.to_account_info(),
ctx.accounts.token_metadata_program.to_account_info(),
ctx.accounts.token_program.to_account_info(),
ctx.accounts.system_program.to_account_info(),
ctx.accounts.rent.to_account_info(),
];
msg!("Master Edition Account Infos Assigned");
invoke(
&create_master_edition_v3(
ctx.accounts.token_metadata_program.key(),
ctx.accounts.master_edition.key(),
ctx.accounts.mint.key(),
ctx.accounts.payer.key(),
ctx.accounts.mint_authority.key(),
ctx.accounts.metadata.key(),
ctx.accounts.payer.key(),
Some(0),
),
master_edition_infos.as_slice(),
)?;
msg!("Master Edition Nft Minted !!!");
Ok(())
}

πŸ’« If you want to debug your program better use msg!() to log whatever value you want to check, it accepts string so you’ll hv to use std::string::ToString to convert. Your logs will appear in the terminal or in .anchor/program-logs/<program-id>

logs path

The creator array needs to have the person minting the NFTs as part of it, but you can set the share as 0 so it doesn’t really matter

let creator = vec![
mpl_token_metadata::state::Creator {
address: creator_key,
verified: false,
share: 100,
},
mpl_token_metadata::state::Creator {
address: ctx.accounts.mint_authority.key(),
verified: false,
share: 0,
},
];

I haven’t implemented Collections as it’s not in the scope of this guide but u can do it by using:

mpl_token_metadata::instruction::set_and_verify_collection

Regarding why I’ve set Max supply to 0 here. In Metaplex if the token is meant to be one of a kind, then you have to set it’s max supply to zero since total supply β€” supply claimed (1–1) equals 0

&create_master_edition_v3(
ctx.accounts.token_metadata_program.key(),
ctx.accounts.master_edition.key(),
ctx.accounts.mint.key(),
ctx.accounts.payer.key(),
ctx.accounts.mint_authority.key(),
ctx.accounts.metadata.key(),
ctx.accounts.payer.key(),
Some(0), // max supply 0
),

Once you’ve written the function run anchor build && anchor deploy and you should see the Program ID

Install program id in terminal

paste this program ID in your Anchor.toml and lib.rs file wherever you see this default ID Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS

Before doing anything make sure that you have imported @solana/web3.js and @solana/spl-token. Inside tests/<test-file>.ts add these imports and constants:

import {
TOKEN_PROGRAM_ID,
createAssociatedTokenAccountInstruction,
getAssociatedTokenAddress,
createInitializeMintInstruction,
MINT_SIZE,
} from "@solana/spl-token";
import { LAMPORTS_PER_SOL } from "@solana/web3.js";
const { PublicKey, SystemProgram } = anchor.web3; q
const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
const lamports: number =
await program.provider.connection.getMinimumBalanceForRentExemption(
MINT_SIZE
);
const getMetadata = async (
mint: anchor.web3.PublicKey
): Promise<anchor.web3.PublicKey> => {
return (
await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
)
)[0];
};
const getMasterEdition = async (
mint: anchor.web3.PublicKey
): Promise<anchor.web3.PublicKey> => {
return (
await anchor.web3.PublicKey.findProgramAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
Buffer.from("edition"),
],
TOKEN_METADATA_PROGRAM_ID
)
)[0];
};
const mintKey: anchor.web3.Keypair = anchor.web3.Keypair.generate();

Now let’s make the token and the associated token account

const NftTokenAccount = await getAssociatedTokenAddress(
mintKey.publicKey,
program.provider.wallet.publicKey
);
console.log("NFT Account: ", NftTokenAccount.toBase58());
const mint_tx = new anchor.web3.Transaction().add(
anchor.web3.SystemProgram.createAccount({
fromPubkey: program.provider.wallet.publicKey,
newAccountPubkey: mintKey.publicKey,
space: MINT_SIZE,
programId: TOKEN_PROGRAM_ID,
lamports,
}),
createInitializeMintInstruction(
mintKey.publicKey,
0,
program.provider.wallet.publicKey,
program.provider.wallet.publicKey
),
createAssociatedTokenAccountInstruction(
program.provider.wallet.publicKey,
NftTokenAccount,
program.provider.wallet.publicKey,
mintKey.publicKey
)
);
const res = await program.provider.send(mint_tx, [mintKey]);
console.log(
await program.provider.connection.getParsedAccountInfo(mintKey.publicKey)
);
console.log("Account: ", res);
console.log("Mint key: ", mintKey.publicKey.toString());
console.log("User: ", program.provider.wallet.publicKey.toString());
const metadataAddress = await getMetadata(mintKey.publicKey);
const masterEdition = await getMasterEdition(mintKey.publicKey);
console.log("Metadata address: ", metadataAddress.toBase58());
console.log("MasterEdition: ", masterEdition.toBase58());

Note: the mint and freeze authority has to be the same otherwise it doesn’t work

createInitializeMintInstruction( mintKey.publicKey, 0, program.provider.wallet.publicKey,// mint auth program.provider.wallet.publicKey // freeze auth
),

Now call the mint function and pass all the data and accounts

const tx = await program.rpc.mintNft(
mintKey.publicKey,
"https://arweave.net/y5e5DJsiwH0s_ayfMwYk-SnrZtVZzHLQDSTZ5dNRUHA",
"NFT Title",
{
accounts: {
mintAuthority: program.provider.wallet.publicKey,
mint: mintKey.publicKey,
tokenAccount: NftTokenAccount,
tokenProgram: TOKEN_PROGRAM_ID,
metadata: metadataAddress,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
payer: program.provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
masterEdition: masterEdition,
},
}
);
console.log("Your transaction signature", tx);

That’s it! Now just run anchor test and you should be able to mint your nft

Account:  4swRFMNovHCkXY3gDgAGBXZwpfFuVyxWpWsgXqbYvoZG1M63nZHxyPRm7KTqAjSdTpHn2ivyPr6jQfxeLsB6a1nX
Mint key: DehGx61vZPYNaMWm9KYdP91UYXXLu1XKoc2CCu3NZFNb
User: 7CtWnYdTNBb3P9eViqSZKUekjcKnMcaasSMC7NbTVKuE
Metadata address: 7ut8YMzGqZAXvRDro8jLKkPnUccdeQxsfzNv1hjzc3Bo
MasterEdition: Au76v2ZDnWSLj23TCu9NRVEYWrbVUq6DAGNnCuALaN6o
Your transaction signature KwEst87H3dZ5GwQ5CDL1JtiRKwcXJKNzyvQShaTLiGxz4HQGsDA7EW6rrhqwbJ2TqQFRWzZFvhfBU1CpyYH7WhH
βœ” Is initialized! (6950ms)
1 passing (7s)✨ Done in 9.22s.

πŸ’« If you get any custom program error with a hex value like 0x1, convert the hex value to plain text then go to metaplex github and search using your browser for the number + 1 th appearance of the word β€œerror(”

You can check out the NFT here:

I hope this guide was useful for all the Solana geeks out there, when I first tried to mint an NFR I was pulling my hair out but it slowly started to make sense once some fellow glass eaters explained it to me, hopefully, I’ve made it that much easier for you.

Here’s the GitHub for this project:

You can follow me on my Twitter and Github until next time keep eating glass!

Big thanks to Pratik Sariya and 0xDeep for helping me understand on how Solana NFTs and Anchor works, if it weren’t for them i’d still be trying to figure it out

Leave a Comment