DEV Community

Jimmy
Jimmy

Posted on • Originally published at jimii.pages.dev

Mint Solana NFT Using Rust & Metaplex SDK

Solana is a public, permissionless blockchain launched in 2020. It leverages computer hardware for its high throughput and performance. Solana has been the pioneer in cutting-edge innovation leading in the adoption of tech like concurrent Merkle trees, interfaces, token-2022...etc

I briefly talked about some differences between Solana and EVM chains previously and in this article I will take you through the process of minting an NFT using the Rust Solana SDK and the MetapleX SDK.

Table of contents

Environmental Set-up

Consider: Solana Playground - A dedicated online playground where you can write and deploy your Solana programs using anchor, native Rust or Seahorse-lang online.

You can also skip this section if you already your Solana environment setup.

Convenient (must have) tool that will make development easier

To install the Solana CLI run the command below,

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
Enter fullscreen mode Exit fullscreen mode

Depending on your system, you might get an installer messaging

Please update your PATH environment variable to include the solana programs:
Enter fullscreen mode Exit fullscreen mode

Follow the installer instructions and Confirm the solana suite is installed properly by running,

solana --version
Enter fullscreen mode Exit fullscreen mode

Next, we need to set up a paper wallet, that we will use to test our programs locally and deploy them.

solana-keygen new
Enter fullscreen mode Exit fullscreen mode

This command creates a new paper wallet in the ~/.config/solana/id.json directory, though you have the option to tell it where to save the file using the -o, --outfile <FILEPATH> flag.

In this tutorial we will be writing a native Rust Solana Program though it's advisable to use the anchor framework.

Why Native? Because at the moment the solana cli and anchor are incompatible.

Introduction to Tokens in Solana

To start off, in Solana, everything is an account. You have data accounts that, you guessed it, store data and executable account(smart contracts) that run code when certain conditions are met.

In the context of tokens/NFTs, Unlike in Ethereum, where you have predefined standards/interfaces for creating a contract that mint's tokens(nfts included), solana has the Token program. That means you do not have to deploy a contract every time you want to mint a new token. This also makes it really easy to mint token using TypeScript or Python, if you have the web3.js Solana SDK.

Lets talk about some terminologies that usually trip up new developers,

  • The Token that you want to create. It can be a Fungible ERC-20 like Token or a non-fungible ERC-721 like NFT.
    How are Solana tokens different? In Solana to represent a non fungible token, usually has 0 decimals meaning it can not be subdivided any further.
    Fungible tokens on the other hand, have the supply greater than one and can have decimals.

  • The mint accounts or token mint is an account that contains information about a specific token type.
    For example, the mint authority(who can create more tokens), freeze authority - who can freeze accounts for a certain mint account, total supply e.t.c
    The mint account does not hold any token. It just defines the rules for the token you are creating.

  • The token account is a general term for any account that holds tokens for a particular mint.

  • The Associated Token Account, refers to a token account that's associated with a particular primary address.
    They are deterministic account that are derived using the address and the mint account.
    For example, say a user has 200 tokens associated with a specific mint. He would need to manage all the keys for these tokens. If the user wanted to send or receive a token, he would need to know the token account for a new address beforehand.
    With ATA the user can send and receive tokens using his main address. Since the ATA is derived deterministically, it'll be guaranteed to just work.

    More in the official documentation.

  • Metaplex is a protocol built on top of Solana to mint and trade NFTs.
    It allows us to attach metadata, images, audio or video to tokens.

  • Mater Edition is a type of NFT that manages and mints a series of prints or edition from a single piece
    of artwork.
    This is what enables us to create collection and map related nfts items from the same collection together.
    For example, if you have an artpiece and create a master edition for it, you could create 100 unique 'prints' each linked back to the master edition carrying a unique edition number like 1 of 100, 20 of 100 etc

  • To Bring it all back together:
    If you want to create an 10 nfts called MyToken, you would instruct the Token Program to create/mint the MyToken by start initializing the mint account. It would be this account that contains details such as the token decimals, in this case 0 and a total supply of 1. These NFTs are stored in the Token Account. When one of these nfts is transferred to a wallet address, an Associated Token Account is created.
    To mint something like a collection of nfts, you would use something like master edition from Metaplex.

It can be hard learning new things, so take a break and revisit these concepts after a few days.

Intro to Native Rust Solana Development.

To get started we will initialize a new empty rust lib project with cargo.

cargo new --lib solana-nft-native
Enter fullscreen mode Exit fullscreen mode

These are the crates that we will need to create our program.

Note: We are using versioned crates because there is a borsh version mismatch between what metaplex and solana-program-library uses.

  • solana-program crate which provides the necessary APIs that allow us to interact with Solana.
  • borsh allows us to efficiently encode and decode data structures in a compact binary format.
  • borsh derive is a procedural macro that allows us to #[derive(BorshSerialize, BorshDeserialize)] our data structs.
  • spl-token to allow gives us access to the spl-token program.
  • spl-associated-token-account allowing us to derive associated token accounts.
  • mpl-token-metadata allowing us to use Metaplex functions to add metadata to our token.
cargo add solana-program@=1.14.20
cargo add borsh@0.9.3
cargo add borsh-derive@0.9.3
cargo add spl-token@3.3.0 --features=no-entrypoint
cargo add spl-associated-token-account@1.0.3 --features=no-entrypoint
cargo add mpl-token-metadata@1.2.5 --features=no-entrypoint
Enter fullscreen mode Exit fullscreen mode

We will also make to our Anchor.toml file.

[lib]
crate-type = ["cdylib", "lib"]
Enter fullscreen mode Exit fullscreen mode

[lib] means that our project, compiles to a library crate, which can be used and linked by other libraries and executable. In the context of our program, we have defined the crate-type options as lib and cdylib, read more

Note: Solana programs must specify the crate-type as cdylib. This enables them to be automatically be discovered and built by the cargo build-spf.

Test everything works properly by building the program.

 cargo build-sbf
Enter fullscreen mode Exit fullscreen mode

If your program builds successfully, let's head into our src/lib.rs file, clear the contents of this file.
populate it with the contents below.

This is a simple hello world program program that logs hello world! when called on a solana cluster like devnet.

use solana_program::{
    account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey,
};

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, World!");

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Let's break down the line:

We get started by bringing into scope the packages that we added using cargo.

use solana_program::{
    account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey,
};
Enter fullscreen mode Exit fullscreen mode
  • account_info::AccountInfo - provides the metadata of an account which it carries, including the data it holds, pubkey, etc.

  • entrypoint - used to mark the entrypoint of a program. Whenever a call is made to a solana program, it's the function in this macro that will be run.

  • entrypoint::ProgramResult - signifies the result of a program. Did it execute properly or did it return an error.

  • msg - used to log messages during the execution of a program since dbg! and println! does not work.
    Keep in mind that:

    • the more frequent your msg calls are, the more computational overhead you will have per transaction.
    • the longer your message length, the more data it will consume.
  • pubkey::Pubkey - solana public key struct. used when you define public key values.

Next we use the entrypoint! macro to define the function that will be called when a tx is made to our program. In our case it is the process_instruction function.

entrypoint!(process_instruction);
Enter fullscreen mode Exit fullscreen mode

Finally we have our process_instruction function.

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, World!");

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Our process_instruction function has three parameters,

  1. _program_id - this the the address of your program. you receive it once you've deployed your program.
  2. _accounts - NOTE: solana programs are stateless. What that means is that there is not data stored on a solana program. Instead you have to create other accounts should you want to write data on-chain and pass them as _accounts arguments. You pass all the account to which you will read/mutate as a slice of accounts.
  3. _instruction_data - This is where you pass in any serialized data that, mutates/writes to accounts that the program interacts with.

Client Side Set-up 1

Let's initialize a new ts project which will act as our client to interact with our program.
I am not a big fan of monorepos so this will be in another directory

mkdir solana-nft-native-client
cd solana-nft-native-client
npm init -y
git init
tsc --init
mkdir src
cd src
touch main.ts utils.ts
Enter fullscreen mode Exit fullscreen mode

Install our versioned ts packages

npm i @metaplex-foundation/mpl-token-metadata@2.13.0 @solana/spl-token@0.3.8 @solana/web3.js@1.78.0  borsh@0.7.0 buffer@6.0.3 fs@0.0.1-security
npm i --save-dev typescript@4.9.5 @types/bn.js@5.1.1
Enter fullscreen mode Exit fullscreen mode

After installing our packages, we will also add an npm command dev, to call ts-node on our client side code. Make sure you have ts-node installed.

. Your finished package.json should resemble,

{
    "scripts": {
        "dev": "ts-node src/main.ts"
    },
    "dependencies": {
        "@metaplex-foundation/mpl-token-metadata": "^2.13.0",
        "@solana/spl-token": "^0.3.8",
        "@solana/web3.js": "^1.78.0",
        "borsh": "^0.7.0",
        "buffer": "^6.0.3",
        "fs": "^0.0.1-security"
    },
    "devDependencies": {
        "@types/bn.js": "^5.1.1",
        "typescript": "^4.9.5"
    }
}
Enter fullscreen mode Exit fullscreen mode

We will also switch our node version to v16.17.1 using nvm. This is because solana-test-validator does not work with anything above v17.

nvm use v16.17.1
Enter fullscreen mode Exit fullscreen mode

Let's write a script to test our hello world program.

First we will create our utility functions to derive our command line wallet Keypair and program Keypair.

NOTE: make sure your PROGRAM_KEYPAIR_PATH and USER_KEYPAIR_PATH paths are set correctly.

Populate your src/utils.ts with,

import { readFileSync } from "fs";
import { Keypair } from "@solana/web3.js";
import { homedir } from "os";

const PROGRAM_KEYPAIR_PATH =
    homedir() +
    "/Documents/web3/solana/solana-nft-native/target/deploy/solana_nft_native-keypair.json";
const USER_KEYPAIR_PATH = homedir() + "/.config/solana/id.json";

export function userKeyPair(): Keypair {
    return Keypair.fromSecretKey(
        Buffer.from(JSON.parse(readFileSync(USER_KEYPAIR_PATH, "utf-8")))
    );
}

export function programKeyPair() {
    return Keypair.fromSecretKey(
        Buffer.from(JSON.parse(readFileSync(PROGRAM_KEYPAIR_PATH, "utf-8")))
    );
}

console.log("user keypair", userKeyPair().publicKey.toString());
console.log("program keypair", programKeyPair().publicKey.toString());
Enter fullscreen mode Exit fullscreen mode

Running the script above with npm run dev should print the two public keys.

Next we will write client code to call our program. If everything runs successfully, then our program should log hello world.

Populate main.ts with

import {
    Connection,
    Keypair,
    PublicKey,
    sendAndConfirmTransaction,
    Transaction,
    TransactionInstruction,
} from "@solana/web3.js";
import { userKeyPair, programKeyPair } from "./utils";

let connection = new Connection("http://localhost:8899");
let programId = programKeyPair().publicKey;
const payer = userKeyPair();

async function helloWorld(): Promise<String> {
    const ix = new TransactionInstruction({
        keys: [],
        programId,
    });

    const tx = new Transaction().add(ix);

    return await sendAndConfirmTransaction(connection, tx, [payer]);
}

helloWorld()
    .then((txSig) => console.log("tx sig: ", txSig))
    .catch((err) => console.error("program execution unsuccessful", err));
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we define three global variables:

  • connection which we use to communicate with the solana network. Right now, it is set to connect to our local solana network.
  • programId is our program's public key
  • payer is our keypair which we will use to pay for transactions.

To understand the function, you must understand how transactions work in solana. In Solana a transaction can contain one of multiple instructions. And what are instruction? Simply, it is a method or functions of a Solana program.

In solana an instruction takes in three arguments, the first being the addresses of the programs the instruction will interact with the programIdand finally data which you want to write to an account. In our ix, the data is omitted because we are not writing to any account.

The sendAndConfirmTransaction method allows us to send and sign transactions.

To ran our client, we will need to set up a local validator. In a new terminal window run

solana-test-validator
Enter fullscreen mode Exit fullscreen mode

Next we will need the solana cli to send our transaction to this test-validator we are running with

solana config set --url localhost
Enter fullscreen mode Exit fullscreen mode

We will now compile our program and deploy it. Run the build-sbf command in your program directory.

cargo build-sbf
Enter fullscreen mode Exit fullscreen mode

We will now deploy our compiled program. This will be the .so (shared object) file in the target/deploy directory.

solana program deploy solana_nft_native.so
Enter fullscreen mode Exit fullscreen mode

Head back to the directory where you wrote your client code.

Open a new terminal window or tab. We will use this to view our program logs

solana logs --url localhost
Enter fullscreen mode Exit fullscreen mode

We will now run our client script and if successful, we should see the logs with the string Hello, World! logged out.
Screenshot of our program logging our Hello World!

With the basics in place, let's head back to where our program code lives and write the code to mint our nft.

More on Native Rust Solana Dev

Clear everything except our package imports. We will start by importing borsh for serialization.

use borsh::{BorshDeserialize, BorshSerialize}; //new
use mpl_token_metadata::instruction as mpl_instruction; //new
use solana_program::{
    account_info::{next_account_info, AccountInfo}, //new
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke,    //new
    pubkey::Pubkey,
    rent::Rent,         //new
    system_instruction, //new
    sysvar::Sysvar,     //new
};
use spl_token::{instruction as token_instruction, state::Mint}; //new

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, World!");

    Ok(())
}

Enter fullscreen mode Exit fullscreen mode

The new imports:

  • borsh::{BorshDeserialize, BorshSerialize}; - serialize and deserialize our data.
  • mpl_token_metadata::instruction as mpl_instruction; - provides functions for us to interact with the Metaplex Token Metadata program.
  • account_info::{next_account_info function is used to parse accounts passed in as input to our Solana program. Remember that Solana need to know before hand the accounts that it will touch, and this is done by providing the addresses of said accounts.
  • program::invoke is used to make Cross Program Invocations. It takes two arguments. The instructions to run and the accounts that it will touch.
  • rent::Rent is used to pay for the storage space we will take up when storing data on the blockchain.
  • system_instruction is a module that allows us to construct system level instructions that directly interact with the Solana blockchain. For example, when creating an account, you would make a call using a system instruction.
  • sysvar::Sysvar - is a module that provides various system variables about the state of the blockchain. They are read only. For example you might want to use the Clock Sysvar that contains information about the epochs and slots
  • spl_token::{instruction as token_instruction, state::Mint}; - provides methods for us to interact with the token program.

When creating our token, we will need to pass in a title, symbol and our metadata uri. This data will need to be serialized and deserialized so that the client and the program can make sense of it. To do this we will use the Serialize and Deserialize borsh traits in a derive attribute.

We will pass on these arguments to the Metaplex Metadata Program

#[derive(BorshSerialize, BorshDeserialize)]
pub struct CreateTokenArgs {
    pub nft_title: String,
    pub nft_symbol: String,
    pub nft_uri: String,
}
Enter fullscreen mode Exit fullscreen mode

Next, let's invoke the function that will call our token and metaplex program. We will call it create_nft.


fn create_nft(accounts: &[AccountInfo], create_token_data: CreateTokenArgs) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();

    let mint_account = next_account_info(accounts_iter)?;
    let mint_authority = next_account_info(accounts_iter)?;
    let metadata_account = next_account_info(accounts_iter)?;
    let payer = next_account_info(accounts_iter)?;
    let rent = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;
    let token_program = next_account_info(accounts_iter)?;
    let token_metadata_program = next_account_info(accounts_iter)?;

    Ok(())
}

Enter fullscreen mode Exit fullscreen mode

The functions takes a slice of type AccountInfo and create a mutable iterator for the accounts input. it is from this accounts that we pull out the accounts that are required to create an nft while interacting with the metadata and token programs.

  • The mint_account contains details about the mint.
  • The mint_authority contains the address of the person who will control the mint
  • The metadata_account as it's name suggests will store the metadata details
  • The payer contains the account that will bare the costs for minting the nft and rent for opening the required accounts.
  • rent. When opening an account, you need to set aside SOL to pay for it. You can recover this once you close an account.
  • system_program This executable accounts/program is what is responsible for opening up new accounts. In Solana a good mental model to think about things is that every program/accounts will always have an owner that controls it.
  • token_program will be the address for our token program, while token_metadata_program will be the address for the token metadata program.

We are now ready to start calling instructions that will create our nft.

// invoke the system program to create our account
    msg!("Creating mint account...");
    msg!("Mint: {}", mint_account.key);
    invoke(
        &system_instruction::create_account(
            &payer.key,
            &mint_account.key,
            (Rent::get()?).minimum_balance(Mint::LEN),
            Mint::LEN as u64,
            &token_program.key,
        ),
        &[
            mint_account.clone(),
            payer.clone(),
            system_program.clone(),
            token_program.clone(),
        ],
    )?;
Enter fullscreen mode Exit fullscreen mode

What is this ↑ piece of code doing? Well, we are using invoke to make a cross program call between our system_program and our token_program. To make a CPI call, you need to pass the instruction you want to call and all the accounts that the call interacts with as arguments.

In the first parameter for our instruction we are calling the create_account instruction from system_program to initialize a new account. These are the parameters we are required to pass to our instruction -

  1. &payer.key Is the public key that will pay for the accounts creation,
  2. &mint_account.key Is the public key for the account being created,
  3. (Rent::get()?).minimum_balance(Mint::LEN), when creating an account in solana, you are required to pay for blockspace. There is not such thing as a free lunch. To store data on Solana in an account, you need to pay for it.
    To note, is that there code above is checking whether our account is rent exempt. We check the minimum_balance required to keep the account rent exempt
    rent exempt is a concept in Solana that allows an account to not incur any rent rent collection for a duration of two years.
    Rent is the program that manages this.

  4. Mint::LEN as u64 is passed in to determine the length/space our account will take. The default size is 82 bytes

  5. &token_program.key is the address of the account that owns or account. In this case it's the token_program.

The next parameter is a slice of the account that invoke will touch.

Next we initialize the created account as the mint account

    //initialize the created account as the mint
    msg!("Initializing mint account...");
    msg!("Mint: {}", mint_account.key);
    invoke(
        &token_instruction::initialize_mint(
            &token_program.key, //token program public key
            &mint_account.key, //mint public key
            &mint_authority.key, //mint authority
            Some(&mint_authority.key), //freeze authority
            0, //decimals
        )?,
        &[
            mint_account.clone(),
            mint_authority.clone(),
            token_program.clone(),
            rent.clone(),
        ],
    )?;
Enter fullscreen mode Exit fullscreen mode

As with the previous invoke call, we pass two arguments to this method. One is the instructions that we will invoke and the second is the accounts that the cross program call will touch.

our token_instruction::initialize_mint takes in 5 parameters, as indicated by the comments.
To note, is the no of decimals. For a token in Solana to be non-fungible, it should not be divisible, hence zero.

To finish off our program, we call the token metadata program from Metaplex. We will use the create_metadata_accounts_v3 instruction to create our Metadata.

 //call Metaplex token metadata program
    msg!("Creating metadata account...");
    msg!("Metadata account address: {}", metadata_account.key);
    invoke(
        &mpl_instruction::create_metadata_accounts_v3(
            *token_metadata_program.key, //program id
            *metadata_account.key,       // metadata address
            *mint_account.key,           //mint address
            *mint_authority.key,         //mint authority address
            *payer.key, //payer public key address
            *mint_authority.key, //update authority address
            create_token_metadata.nft_title, //nft title
            create_token_metadata.nft_symbol, //nft symbol
            create_token_metadata.nft_uri, //nft uri
            None, //creators. for something you pass in a VEC of their addresses
            0, //royalties
            true, //is the payer the update authority
            false, //can we update the token metadata
            None, //collection the nft belongs to. struct of pubkey and
            None, //uses
            None, //collection details
        ),
        &[
            metadata_account.clone(),
            mint_account.clone(),
            mint_authority.clone(),
            payer.clone(),
            token_metadata_program.clone(),
            rent.clone(),
        ],
    )?;

    msg!("Token mint created successfully.");
Enter fullscreen mode Exit fullscreen mode

The code pattern is the same as the previous one. We first call invoke with two arguments. The instruction and the accounts we touch.

The create_metadata_accounts_v3 will create our metadata account and write to it our metadata information. Each argument is commented to show what it represents.

To finally, finally finish off our program, we being our create_token into our process_instruction function which will now look like this

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let token_metadata = CreateTokenArgs::try_from_slice(instruction_data)?; //new

    create_nft(accounts, token_metadata)?;

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

The method is not something that is new. What is new, is try_from_slice. Remember that to efficiently sent messages to and from Solana and reduce computational overhead, your data needs to be serialized to a format(binary format) that can efficiently do this.

Thetry_from_slice method is part of the BorshDeserialize trait, and it's used to convert a byte slice (&[u8]) into an instance of our CreateTokenArgs.

And yes, You will need to serialize data from the client side too. This is why a lot of dev prefer using anchor as it automatically handles this boilerplate of serializing and deserializing data.

Altogether your src/lib.rs should resemble.

use borsh::{BorshDeserialize, BorshSerialize};
use mpl_token_metadata::instruction as mpl_instruction;
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke,
    program_pack::Pack,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction,
    sysvar::Sysvar,
};
use spl_token::{instruction as token_instruction, state::Mint};

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let token_metadata = CreateTokenArgs::try_from_slice(instruction_data)?;

    create_nft(accounts, token_metadata)?;

    Ok(())
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct CreateTokenArgs {
    pub nft_title: String,
    pub nft_symbol: String,
    pub nft_uri: String,
}

fn create_nft(accounts: &[AccountInfo], create_token_metadata: CreateTokenArgs) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();

    //pick out our accounts
    let mint_account = next_account_info(accounts_iter)?;
    let mint_authority = next_account_info(accounts_iter)?;
    let metadata_account = next_account_info(accounts_iter)?;
    let payer = next_account_info(accounts_iter)?;
    let rent = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;
    let token_program = next_account_info(accounts_iter)?;
    let token_metadata_program = next_account_info(accounts_iter)?;

    // invoke the system program to create our account
    msg!("Creating mint account...");
    msg!("Mint: {}", mint_account.key);
    invoke(
        &system_instruction::create_account(
            &payer.key,
            &mint_account.key,
            (Rent::get()?).minimum_balance(Mint::LEN),
            Mint::LEN as u64,
            &token_program.key,
        ),
        &[
            mint_account.clone(),
            payer.clone(),
            system_program.clone(),
            token_program.clone(),
        ],
    )?;

    //initialize the created account as the mint
    msg!("Initializing mint account...");
    msg!("Mint: {}", mint_account.key);
    invoke(
        &token_instruction::initialize_mint(
            &token_program.key,
            &mint_account.key,
            &mint_authority.key,
            Some(&mint_authority.key),
            0,
        )?,
        &[
            mint_account.clone(),
            mint_authority.clone(),
            token_program.clone(),
            rent.clone(),
        ],
    )?;

    //call Metaplex token metadata program
    msg!("Creating metadata account...");
    msg!("Metadata account address: {}", metadata_account.key);
    invoke(
        &mpl_instruction::create_metadata_accounts_v3(
            *token_metadata_program.key,      //program id
            *metadata_account.key,            // metadata address
            *mint_account.key,                //mint address
            *mint_authority.key,              //mint authority address
            *payer.key,                       //payer public key address
            *mint_authority.key,              //update authority address
            create_token_metadata.nft_title,  //nft title
            create_token_metadata.nft_symbol, //nft symbol
            create_token_metadata.nft_uri,    //nft uri
            None,  //creators. for something you pass in a VEC of their addresses
            0,     //royalties
            true,  //is the payer the update authority
            false, //can we update the token metadata
            None,  //collection the nft belongs to. struct of pubkey and
            None,  //uses
            None,  //collection details
        ),
        &[
            metadata_account.clone(),
            mint_account.clone(),
            mint_authority.clone(),
            payer.clone(),
            token_metadata_program.clone(),
            rent.clone(),
        ],
    )?;

    msg!("Token mint created successfully.");

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

To finish off our program side development, we are going to build and deploy our program on devnet.

solana config set --url devnet
cargo build-sbf
solana program deploy solana_nft_native.so
Enter fullscreen mode Exit fullscreen mode

Note: Your so file is in the target/deploy directory.

Client Side Set-up 2

We are almost at the end of our journey of creating nfts for Solana.

Going back to where you set up your client code before. You can comment out the helloWorld function we wrote. We will create a class to serialize our data.

class TokenArgs {
    nft_title: string;
    nft_symbol: string;
    nft_uri: string;

    constructor(nft_title: string, nft_symbol: string, nft_uri: string) {
        this.nft_title = nft_title;
        this.nft_symbol = nft_symbol;
        this.nft_uri = nft_uri;
    }

    serializeCreateTokenData(data: TokenArgs): Uint8Array {
        const schema = new Map([
            [
                TokenArgs,
                {
                    kind: "struct",
                    fields: [
                        ["nft_title", "string"],
                        ["nft_symbol", "string"],
                        ["nft_uri", "string"],
                    ],
                },
            ],
        ]);
        return Buffer.from(borsh.serialize(schema, data));
    }
}
Enter fullscreen mode Exit fullscreen mode

We have abstracted the logic to serializing our data to the TokenArgs class.

In it we have our constructor which sets our nft_title, nft_symbol and nft_uri member variables.

Next we have our serializeTokenData method which contains our borsh schema and it's this methods that serializes our data, ready to be send to our program.

We then set up our createToken function. We will create our mint accounts keypair using Keypair.generate(); functions and derive a pda which will store our metadata. The seeds to do so are metadata, TOKEN_METADATA_PROGRAM_ID and mintKeypair.publicKey.

import { PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata";

const mintKeypair: Keypair = Keypair.generate();

const metadataAddress = PublicKey.findProgramAddressSync(
    [
        Buffer.from("metadata"),
        TOKEN_METADATA_PROGRAM_ID.toBuffer(),
        mintKeypair.publicKey.toBuffer(),
    ],
    TOKEN_METADATA_PROGRAM_ID
)[0];
Enter fullscreen mode Exit fullscreen mode

Let finish off by declaring our data variables and assigning them appropriately.

async function createToken(): Promise<String> {
    let connection = new Connection("http://localhost:8899");
    let programId = programKeyPair().publicKey;
    const payer = userKeyPair();

    const mintKeypair: Keypair = Keypair.generate();

    const metadataAddress = PublicKey.findProgramAddressSync(
        [
            Buffer.from("metadata"),
            TOKEN_METADATA_PROGRAM_ID.toBuffer(),
            mintKeypair.publicKey.toBuffer(),
        ],
        TOKEN_METADATA_PROGRAM_ID
    )[0];

    const instructionData = new TokenArgs(
        "Kobeni",
        "KBN",
        "https://raw.githubusercontent.com/687c/solana-nft-native-client/main/metadata.json"
    );

    let ix = new TransactionInstruction({
        keys: [
            { pubkey: mintKeypair.publicKey, isSigner: true, isWritable: true }, // Mint account
            { pubkey: payer.publicKey, isSigner: false, isWritable: true }, // Mint authority account
            { pubkey: metadataAddress, isSigner: false, isWritable: true }, // Metadata account
            { pubkey: payer.publicKey, isSigner: true, isWritable: true }, // Payer
            { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false }, // Rent account
            {
                pubkey: SystemProgram.programId,
                isSigner: false,
                isWritable: false,
            }, // System program
            { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, // Token program
            {
                pubkey: TOKEN_METADATA_PROGRAM_ID,
                isSigner: false,
                isWritable: false,
            }, // Token metadata program
        ],
        data: instructionData.serialize(),
        programId: programId,
    });

    const modifyComputeUnits = ComputeBudgetProgram.setComputeUnitLimit({
        units: 1000000,
    });

    const tx = new Transaction().add(modifyComputeUnits).add(ix);

    return await sendAndConfirmTransaction(connection, tx, [
        payer,
        mintKeypair,
    ]);
}
Enter fullscreen mode Exit fullscreen mode

In our script above, we modify our tx and increase our compute units. This will prevent us from running into unwanted, compute limit exceeded errors.

Note:, you need to pass in all account that sign, in the signers array. In our case, these are, payer and mintKeypair.

createToken()
    .then((txSig) =>
        console.log(`tx hash: https://solscan.io/tx/${txSig}?cluster=devnet`)
    )
    .catch((err) => console.error("program execution unsuccessful", err));
Enter fullscreen mode Exit fullscreen mode

And finally, finally, let's call our client script, npm run dev.

Open the link returned after running your script. It should take you to the solscan explorer.

Screenshot of the result of calling our program!

To look at our token on solscan, we will click on the second signer. The one surrounded by a yellow border. That is our token mint account.

Open the second account and it should show you the details about the nft you created.

Screenshot of our nft page on solscan!

Find the code for the client side here and for the Solana program here.

And finally, huge shout out to the Solana program examples repository from where the code for this article was lifted. 😂

In the next part we modify our code to mint the tokens directly to our wallet or a specified address.

Further Reading

Top comments (2)

Collapse
 
parth51199 profile image
parth51199

what programming language is used to create the Solana NFTs using the Metaplex SDK?

Collapse
 
jimii profile image
Jimmy

Rust.

But it's possible to use TypeScript