This tutorial will guide you on how to deploy an NFT contract on Solana Devnet and achieve interoperability with NFTs deployed on Solana Devnet through Grid.

Mint Solana NFTs Using Anchor and Metaplex

Note: when reading this article a // snip or # snip comment means that the code has been cut.

Environmental Set-up

You can skip this setup if your Solana environment is set up properly.
For the uninitiated in Solana, we will get started by first installing the Solana Suite which is provided as a CLI suite.

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

Depending on your system, you might get an installer messaging:

Please update your PATH environment variable to include the solana programs:

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

solana --version

We will finally install the anchor-cli which is provided by the anchor version manager. Find the guide on this official page.
Install avm using cargo,

cargo install --git https://github.com/coral-xyz/anchor avm --locked --force

Use avm to install anchor cli

avm install latest  
avm use latest

confirm anchor is installed properly by running

anchor --version

At the time of writing this, here is what my environment looks like, after running anchor --version && solana --version

anchor-cli 0.28.0  
solana-cli 1.16.13 (src:b2a38f65; feat:3949673676, client:SolanaLabs)

Once everything is set up properly, we can get started using the anchor cli to initialize our new anchor project.

anchor init solana-nft-anchor

Change into your created anchor project and open it up with your preferred text editor. In our case, using vs code, we’ll run

cd solana-nft-anchor
code .

The first thing we will need is to enable init_if_needed the feature for anchor-lang crate. This feature allows us to create an account if it isn’t created automatically for us.

cargo add anchor-lang --features=init-if-needed

We will also install the anchor-spl crate with metadata features. The anchor-spl create provides us with almost all the necessary functionality required to interact with the Token Program while the metadata feature brings into scope the functionality required to interact the Metaplex’s Token Metadata Program.

cargo add anchor-spl --features=metadata

Running a quick anchor build, ensures that everything is working in tandem with each other and we are not dealing with dependency conflicts.
We are now ready to start writing our anchor program to mint NFTs for us. Open the programs/solana-nft-anchor/lib.rs file and it should have contents similar to this

use anchor_lang::prelude::*;

declare_id!("HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps"); // should'nt be similar to mine

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

If you encounter this error struct takes 0 lifetime arguments but lifetime argument was supplied expected 0 lifetime arguments, you can ignore it as it will go away as soon as we start working on our code.
We will rename Initialize to InitNFT and bring into scope the accounts we’ll interact with when creating our NFT in the Accounts struct.
The first account we will bring into scope will be the signer. This account is the authority, fee payer for the transactions we make and signer of the transaction.

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    signer: AccountInfo<'info>
}

But how does Solana parallel program execution layer(Sealevel) differentiate a signer account from a normal one? We mark it as one. There are two ways to do this, using the Signer account variant or by using anchor constraints which is what we are doing. Anchor constraints defined using #[account(<constraints>)] are built-in features to simplify common security checks, e.g. mark accounts as mutable or not.
In our code snipped above, we have marked the account as a mutable account(because we are going to be mutating the account balance when paying for the transactions) and as a signer using #[account(mut, signer)]
To also note is the use of Rustdoc comment /// CHECK: ok, we are passing in this account ourselves. This is vital when using the AccountInfo wrapper. We will discuss this after introducing the Account wrapper in the next section.

Interacting With the Token Program

To create our NFT we will need to interact with the token program and the metadata program. When working in Anchor and Solana, it is required to explicitly declare all the accounts which you’ll interact with.
This brings us to the second account which we bring into scope, the Mint account. The mint account of a token contains details about the token such as mint authority, freeze authority, total supply …etc.

The Mint Account

Owner: Token Program 
Mint Authority = Edition 
Supply = 1
Decimals = 0
Freeze Authority = Edition 
use anchor_spl::token::Mint;

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[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>,
}

We have also added more constraints to our mint account. Let’s go over them. The first constraint init is like “a wrapper” around the system_instruction::create_account() functions which instruct the System Program to create the account. this initialization takes place in three steps: allocate space, transfer Lamports for rent and assign the account to the owning program. This is where the second constraint comes in payer = signer is used to pay the rent for the account creation. What is rent? For you to store data on Solana, you must pay a sort of deposit. This incentivizes the validators to store your data. If not paid, your data will be pruned from the blockchain. The next set of constraints mint::decimals = 0 sets the decimals of our NFT token. You can’t have a 0.25 NFT! Finally, we set the mint::authority = signer.key(), mint::freeze_authority = signer.key(), field to our address.

Looking at this newly added account, the declaration is different from the first account. We are now using the Account account type instead of AccountInfo.

The anchor AccountInfo type is a way to define accounts that do not implement any checks on the account being passed. We are blindly trusting the account being passed as the correct account without verifying the structure of the data or the owner of the account. As such we have to also explicitly mark it as trustworthy using the rustdoc comments /// CHECK: <comment explaining why are we blindly trusting this account>.

The Account account type is a more secure way of declaring your accounts. It contains all the methods ofAccountInfo, but it verifies program ownership and deserializes underlying data into the specified type. Our above, Account checks that the owner of the our mint is indeed the Token program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA and that the account contains all the required fields for a Mint account.

The fields contained that the Mint account contains

  • mint_authority - Address allowed to mint more tokens. For our NFT, this field is going to be set to zero. No one is allowed to mint more tokens. (useful for fungible tokens that have an unlimited supply)
  • supply: The total supply of tokens. In our case when minting an NFT, the supply is going to be set to 1.
  • decimals: Number of decimals to consider when interpreting the balance of the token. For our NFT this is going to be zero. Why? Because there’s no such thing as having 0.5 NFT.
  • is_initialized: Has the token mint been initialized? True, when we call the mint instruction.
  • freeze_authority: Address allowed to freeze the token account.

Note: The mint does not hold any tokens.

If the mint account does not hold your tokens then where are they stored? When creating a token on Solana, you need the Token account to hold your newly minted tokens. The token account contains fields such as

  • mint - The mint address associated with this account.
  • owner - The address that has authority over this account.
  • amount - The number of tokens in this account.
  • delegate - Address of another address that can manage your token account. That means, transferring or freezing your asset.
  • state - The account state. It is an enum of three possible values, Uninitialized, for when the account does not exist, Initialized, for when the account has been created and exists, Frozen, for when the account has been frozen by the freeze authority.
  • is_native, is this token the native Solana token.
  • delegated_amount - amount delegated to the delegate field mentioned above.
  • close_authority - Address which can close this account.

However, there is a downside to this method.

The first downside. Suppose you are an NFT hoarder, having collected 1000 NFT. When your friend wants to send you an NFT from a mint which you already own, he will need to know the correct token account to send this NFT. That means, keeping track of all those 1000 token accounts.

The second downside. Suppose you want to introduce your non-crypto native friend to NFTs. Your friend has never minted from the collection before. If you want to send him his first NFT from a collection he has never minted, your friend needs to have a token account from that Mint’s NFT. This makes transferring assets difficult and cumbersome. It also means that airdrop campaigns become impossible.

This is where the motivation to reduce the friction when working with Solana tokens came in, which led to spec a new way for the token account to map to the user’s wallet, using the Associated Token Account.

The Associated Token Account is a PDA that is deterministically derived using the address and mint account.

Let’s take the accounts required for the mint

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{Mint, Token, TokenAccount},
};

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub mint: Account<'info, Mint>, // new
    #[account(
        init_if_needed,
        payer = signer,
        associated_token::mint = mint,
        associated_token::authority = signer,
    )]
    pub associated_token_account: Account<'info, TokenAccount>, // new

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

We have the accounts we will be interacting with when creating our mint and token accounts.

The mint account is defined by the Mint Account type.

We have an associated_token_account, with multiple anchor constraints. We use the init_if_needed flag to initialize this token account, if it does not exist in our wallet and to use this feature you need to define a payer who will cover the cost associated with creating a new account. We also pass in the authority and mint as constraints, to link the mint to the token account.

I have also added a space to serve as a visual separation between the accounts and Programs. Remember(I know I sound like a broken record, but it’s important to remember this), that everything on Solana is an account. These Programs are also accounts. The difference is that they have their executable field marked as true.Read more about the Solana account model.

Anchor also provides another primitive to make working with Programs easier, We use Program to mark an account as executable. This also implements similar checks to Accounts. This means you can’t pass in the address of a malicious program trying to pass it off as the Token Program. The IDs won’t match and the program executions will be halted.

Let’s talk about the four programs.

  1. token_program - the Token program
  2. associated_token_program - handles creation of our ATA(Associated Token Account).
  3. system_program - because the associated token program might end up needing to create a new ATA, we need to pass in this program which is responsible for creating all accounts.
  4. rent - on Solana, you need to pay for space when you are storing data on the blockchain. All accounts on Solana(now) are required to be rent-exempt, which means putting down a 2-years worth of sol to store data on the chain. (not that expensive by the way). As such we need to interact with the rent program for this.

With that, we are now ready to call the instructions to create our first NFT on Solana. Let us first call the instructions to create the mint and token account. To do this we will need to interact with the Token Program. One way to do this would be to use invoke and make a cross-program call into it. Using invoke is tricky because it requires you to pass in the correct number of accounts and the correct accounts you are going to interact with. . You miss one account and boom, your TX fails and you get a cryptic error telling you, “program failed because you missed an account” and yet it can’t tell you which account you missed . Enough of my rant.

Anchor simplifies this for us, by having a CpiContext struct which encapsulates all accounts that we need to interact with when making our CPI call. It also has already defined methods for commonly called instructions.

Let’s take a look at how we can use the CpiContext to initialize a token mint. To initialize the CpiContext, call the associative function new which takes in two arguments.

  1. The external program we are cpi-ing into.
  2. Anchor defined accounts we will pass in to make the call to the external program successful. As opposed to a normal invoke invocation to an external program, using anchor defined Accounts means we will only declare the account which are vital and not all the programs which almost always never change.

For example, let’s take a look at the code to initialize a new token mint and associated token account using it.

pub fn init_nft(ctx: Context<InitNFT>) -> Result<()> {
        // create mint account
        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)?;
        Ok(())
    }

As highlighted above, we first create our cpi context by calling CpiContext::new() the method. We are interacting with the token program and thus we pass it in first. For the second send we will pass in a struct that contains defines the accounts we will interact with.

Here is all the code together.

use anchor_lang::prelude::*;
use anchor_spl::{
    associated_token::AssociatedToken,
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
};

declare_id!("HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps"); // shouldn't be similar to mine

#[program]
pub mod solana_nft_anchor {

    use super::*;

    pub fn init_nft(ctx: Context<InitNFT>) -> Result<()> {
        // create mint account
        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)?;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub 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>,

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

Calling this method as it is would create our NFT, but what good is a NFT non-fungible without the NFT image? In the next section, we dive into using the Metaplex Token Metadata Program.

Interacting With Metaplex Metadata Program

Metaplex offers a collection of tools, smart contracts and more, designed to make the process of creating and launching NFTs easier.

In this guide, we shall be using their Token Metadata Program to add metadata to our spl-token.

But before that, we need to understand how Metaplex works under the hood. Remember ATAs(Associated Token Account). They are part of a special type of accounts owned and controlled by a program (smart contract) known as Program Derived Accounts. Simply put, they are public keys that do not have a corresponding public key. They have various use cases such as signing transactions, storing SOL and storing data, as used seen in ATAs. They are usually derived using the program’s public key and seeds which are chosen by the developer and passed into the find_program_address() function as bytes. This sha256 hash function looks for an address that is not on the ed25519 elliptic curve (addresses on the curve are keypairs).Further details about PDAs can be found here.

The Metaplex Metadata Program also uses PDAs. Like the Associated Token Program, the Metadata Program uses PDAs for the metadata account that attaches itself to the Mint Account. The metadata account is derived using the following seeds, metadata, Token Metadata Program pubkey i.e metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s and finally the public key of the mint account. Anchor has a different way of deriving PDAs but here is the code snippet for doing the above using a native Solana Rust program.

let metadata_seeds = &[PREFIX.as_bytes(), program_id.as_ref(), mint_pubkey.as_ref()];
let (pubkey, _) = Pubkey::find_program_address(metadata_seeds, &ID);

Now that we know how to derive the metadata account let’s look at the fields that this account holds, show in the image below.

The Metadata Account

Owner: Token Metadata Program  
Key = MetadataV1  
Update Authority  
Mint  
Name  
Symbol  
URI  
Seller Fee Basis Points  
Creators  
Primary Sale Happened  
Is Mutable  
Edition Nonce  
Token Standard  
Collection  
Uses  
Collection Details  
Programmable Configs  

key. This is the first field and it is an enum that lets Metaplex identify the ‘type of metaplex account’, to work with. It similar to the anchor discriminator. Why is it needed. The token program accesses different accounts such as the edition account for print editions of an NFT, the token record for programmable NFTs e.t.c. In our case for the, metadata account, this field is marked with the MetadataV1 enum variant.

Here is an exhaustive list of these markers and accounts and an explanation of what they are used for.

Uninitialized: The account does not exist yet and needs to be created.

  • MetadataV1 This account holds the token metadata(the one we’re currently working with).
  • MasterEditionV1 and MasterEditionV2 The master edition account allows NFTs to be printed a limited or unlimited times amount of times. When we say printed, what we mean is making copies of the NFT
  • EditionV1 The edition account derived from a mint account represents an NFT that was copied from a Master Edition NFT.
  • EditionMarker and EditionMarkerV2 This account is used to internally keep track of which editions were printed and which ones have not.
  • TokenRecord used by programmable NFTs only. Token Record accounts enable us to attach custom data to token accounts rather than mint account.
  • MetadataDelegate These accounts are used to store multiple delegate authorities for a given metadata account.
  • CollectionAuthorityRecord Keeps track of which authorities are allowed to set and/or verify the collection of the metadata account.
  • UseAuthorityRecord Keeps track of which authorities are allowed to reduce the uses(might be deprecated soon) field on the metadata account.
  • TokenOwnedEscrow An escrow account that is managed by the holder of the NFT.
  • ReservationListV1 and ReservationListV2 (DEPRECATED) Used for reservation lists. if present on the list you can get an edition number given by your position on the list.

update_authority - address allowed to make changes to the metadata account.

mint - address of the mint account.

data - the asset data for with the token. This includes values such as the name, symbol, uri, creator royalties and the creators

primary_sale_happened - indicates whether the token has already been sold at least once.

is_mutable - indicates whether the metadata account can ever be changed.

edition_nonce - nonce used to verify the edition nonce’s of printed NFTs.

token_standard - the type of token that the metadata account holds. It is an optional enum that consists of the following variants

  • NonFungible - A non-fungible with a master edition.
  • FungibleAsset (semi fungible) - An spl token with a supply > 1, but which has NFT attributes such as an image and an attributes array.
  • Fungible - A token with metadata and supply > 1.
  • ProgrammableNonFungible - Special type of NonFungible that’s frozen al times and enforces custom authorization rules.

collection - an optional struct that contains the public key of a collectionNft or None if not present.

uses - Another optional field that makes NFTs usable. Meaning you can load it with a certain amount of “uses” and use it until it has run out.

There’s a proposal to deprecate it soon.

collection_details - An optional field with a V1 field that contains the number of NFTs for an NFT collection.

It’s deprecated and might be removed soon

programmable_config - Also an optional field that if set, contains the address to an optional rule set account which contains constraints pertaining to the programmable non-fungible.

Enough talk, let’s Buidl!!!!

In our InitNFT struct we will bring into scope the account we need to add metadata to our spl-token. These are, metadata_account to store our metadata, master_edition_account to set up the master edition NFT. We haven’t talked about the master edition account. Why is it important? It proves the non-fungibility of our token. It will check if the decimals field on the mint account is indeed zero and that the supply field is set to 1. It is also useful in determining if we can make a print edition from the NFT. We will finally bring into scope the token_metadata_program which is responsible for processing the instructions to create both the accounts and instantiate them correctly with the fields we provide.

To put the above paragraphs into code, we first install a version mpl-token-metadata crate.

cargo add mpl-token-metadata@1.13.1


use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{Metadata}, // new
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
};
use mpl_token_metadata::{
    pda::{find_master_edition_account, find_metadata_account}, // new
};

// snip

#[derive(Accounts)]
pub struct InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub 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 - address
    #[account(
        mut,
        address=find_metadata_account(&mint.key()).0,
    )]
    pub metadata_account: AccountInfo<'info>, // new
    /// CHECK: address
    #[account(
        mut,
        address=find_master_edition_account(&mint.key()).0,
    )]
    pub master_edition_account: AccountInfo<'info>, // new

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

To make sure that the right accounts are passed in, we are using the address="" constraint to make sure that the accounts passed in are indeed the metadata and master edition accounts respectively. As we did before, untyped accounts should be accompanied by a rustdoc comment /// CHECK: <reason why the account is untyped> explaining why it is untyped. You might be wondering why we did not use the typed metadata Account<'info, MetadataAccount> and master edition Account<'info, MasterEditionAccount> account. That is because anchor expects those account types to be initialized beforehand.

After bringing the needed accounts and program into scope, we now need to instantiate them. The anchor-spl with metadata crate comes with a useful number of Cross Program Invocation(CPI) instructions we can use.

As we did with the mint account we will be using the CpiContext::new() method to help us make sure we have all the required accounts when making the CPI call to the metadata_program.

We will use the create_metadata_accounts_v3() to create the metadata account. It takes in five arguments,

  • Our CpiContext struct with the required program id and accounts
  • Our asset data aptly named as DataV2. name, symbol, uri … are all defined here.
  • is_mutable, a boolean determining on whether we can make changes to our metadata_account.
  • update_authority_is_signer, a boolean on whether the update authority is going to be the signer creating this transaction.
  • collection details, optional field containing no. of NFTs in our collection.
// snip
use anchor_spl::{
associated_token::AssociatedToken,
metadata::{
create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3,
CreateMetadataAccountsV3, Metadata, MetadataAccount,
}, // new
token::{mint_to, Mint, MintTo, Token, TokenAccount},
};
use mpl_token_metadata::{
pda::{find_master_edition_account, find_metadata_account},
state::DataV2 // new
};

// snip

pub fn init_nft(
ctx: Context<InitNFT>,
name: String,   // new
symbol: String, // new
uri: String,    // new
) -> Result<()> {
// create mint account
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)?;

// create metadata account
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: name,
    symbol: symbol,
    uri: uri,
    seller_fee_basis_points: 0,
    creators: None,
    collection: None,
    uses: None,
};

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

Ok(())

}

Because the DataV2 struct need to take in the asset data, we have changed the definition of our init_nft function to also include the name, symbol and uri parameter which we are passing as our asset data. This is also the same way we would get the other fields in the DataV2 struct such as the seller_fee_basis_points, creators if there were more than one … e.t.c.

Finally we finish off our program by creating the master edition account, by invoking the create_master_edition_v3 instruction, which takes in the accounts needed to initialize the master edition account and an optional max_supply argument. The max_supply takes in the maximum number of editions that can be printed from this NFT. We do not want to allow print editions to be made from our NFT and we set it to None.

use anchor_spl::{
    associated_token::AssociatedToken,
    metadata::{
        create_master_edition_v3, create_metadata_accounts_v3, CreateMasterEditionV3,
        CreateMetadataAccountsV3, Metadata,
    }, // new
    token::{mint_to, Mint, MintTo, Token, TokenAccount},
};

// snip

//create master edition account
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)?;

In ~115 LOC we have written a program that mints an NFT for us on-chain. Here is what the full finished program looks like.

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,
};

declare_id!("HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps"); // shouldn't be similar to mine

#[program]
pub mod solana_nft_anchor {

    use super::*;

    pub fn init_nft(
        ctx: Context<InitNFT>,
        name: String,
        symbol: String,
        uri: String,
    ) -> Result<()> {
        // create mint account
        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)?;

        // create metadata account
        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: name,
            symbol: symbol,
            uri: uri,
            seller_fee_basis_points: 0,
            creators: None,
            collection: None,
            uses: None,
        };

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

        //create master edition account
        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 InitNFT<'info> {
    /// CHECK: ok, we are passing in this account ourselves
    #[account(mut, signer)]
    pub signer: AccountInfo<'info>,
    #[account(
        init,
        payer = signer,
        mint::decimals = 0,
        mint::authority = signer.key(),
        mint::freeze_authority = signer.key(),
    )]
    pub 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 - address
    #[account(
        mut,
        address=find_metadata_account(&mint.key()).0,
    )]
    pub metadata_account: AccountInfo<'info>,
    /// CHECK: address
    #[account(
        mut,
        address=find_master_edition_account(&mint.key()).0,
    )]
    pub master_edition_account: AccountInfo<'info>,

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

To finish off the program side of development, we are going to build and deploy our program. Let’s first configure our Anchor.toml, to deploy to Devnet when we run the deploy command.

To do this we will change the cluster point to devnet

# snip

[provider]
cluster = "devnet"
# snip

We are now ready to deploy to devnet. Go ahead and build your program and deploy it using the following commands.

anchor build

anchor deploy

If commands run accordingly, you should be greeted with a Deploy success message.

Synchronize NFTs from the Base Layer to Hypergrid

We have successfully deployed the NFT program on the Solana Devnet and completed the minting process. Suppose we have an NFT token address: AgBbwKwCmWwgRyPpwtVMj4szRTmRcVwugwCm4iGxTpBj.You can view its details through Solana Explorer. Next, we will switch the network to Hypergrid and synchronize NFTs from the Base Layer to Hypergrid.

1

Step 1:

Open the Backpack wallet and use the private key of the wallet you used to mint this NFT to restore your wallet account. Switch the RPC to: https://api.devnet.solana.com.

You can see the record of the NFT you just minted in the Collectibles section.

2

Step 2:

We will attempt to fetch our NFT HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps, which we have published on the Solana Devnet, on a Grid( RPC is https://rpc.hypergrid.dev/ ) and complete the minting operation. Here’s how we can achieve this using TypeScript:

  async function mintNft(metadata: any) {
  if (isLoading) return;
  setIsLoading(true);
  try {
    let provider: any = anchor.getProvider();
    const program = new anchor.Program(idl as anchor.Idl, "HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps");
    const signer = provider.wallet;
    const umi = createUmi("https://rpc.hypergrid.dev").use(walletAdapterIdentity(signer)).use(mplTokenMetadata());
    const associatedTokenAccount = await getAssociatedTokenAddress(newAccount.publicKey, signer.publicKey);
    let metadataAccount = findMetadataPda(umi, {
      mint: publicKey(newAccount.publicKey)
    })[0];
    let masterEditionAccount = findMasterEditionPda(umi, {
      mint: publicKey(newAccount.publicKey)
    })[0];

    const initdata = {
      signer: provider.publicKey,
      mint: newAccount.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
    };

    const tx = await program.methods
      .initNft(metadata.name, metadata.symbol, metadata.uri)
      .accounts(initdata)
      .signers([newAccount])
      .rpc();

    let account = '';
    let txhash = '';
    if ("https://rpc.hypergrid.dev" == Devnet.value) {
      txhash = `${currentNet.explorer}/tx/${tx}?cluster=devnet`;
      account = `${currentNet.explorer}/address/${newAccount.publicKey}?cluster=devnet`;
    } else {
      txhash = `${currentNet.explorer}/tx/${tx}?cluster=custom&customUrl=${"https://rpc.hypergrid.dev"}`;
      account = `${currentNet.explorer}/address/${newAccount.publicKey}?cluster=custom&customUrl=${"https://rpc.hypergrid.dev"}`;
    }
    console.log(`mint nft tx: `, txhash);
    setMintNftTX(txhash);

    console.log(`mint Account: `, account);
    setMintNftAccount(account);

    setIsLoading(false);
    openMintSuccess();
  } catch (error) {
    console.error(error);
    setIsLoading(false);
    toast({ title: 'Mint nft failed', status: 'error' });
  }
}

For more detailed source code, please visit:
https://github.com/mirrorworld-universe/hypergrid-integrated-demo/blob/main/App/sonic-mint-demo/pages/read/index.tsx#L129

Sure, this mint operation will fail because the NFT program (HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps) does not exist on the Grid.

3

Step 3:

Next, we will start trying to synchronize the NFT program (HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps) from the Solana Devnet to the Grid cache, and then try the mint operation again. Here is a snippet of the TypeScript code:

function createInstructionData(index) {
const dataLayout = BufferLayout.struct([BufferLayout.u32('instruction')]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode({ instruction: index }, data);
return data;
}

async function syncRequest() {
if (isLoading) return;
setIsLoading(true);
try {
  const transaction = new Transaction();
  const instruction1 = new TransactionInstruction({
    keys: [
      { pubkey: new PublicKey("HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps"), isSigner: false, isWritable: false },
      { pubkey: new PublicKey(MPL_TOKEN_METADATA_PROGRAM_ID), isSigner: false, isWritable: false }
    ],
    programId: new PublicKey(syncProgramId),
    data: createInstructionData(0)
  });
  transaction.add(instruction1);

  let provider = anchor.getProvider();
  const tx = await provider.sendAndConfirm(transaction);
  const txhash = `https://explorer.hypergrid.dev/tx/${tx}?cluster=custom&customUrl=https://rpc.hypergrid.dev`;
  console.log(`sync request tx: `, txhash);
  setSyncRequestTX(txhash);

  setIsLoading(false);
  setSyncStatus(true);
  openSyncSuccess();
} catch (error) {
  console.error(error);
  setIsLoading(false);
  toast({ title: 'Sync request failed', status: 'error' });
  }
}

For more detailed source code, please visit:
https://github.com/mirrorworld-universe/hypergrid-integrated-demo/blob/main/App/sonic-mint-demo/pages/read/index.tsx#L197

4

Step 4:

When we complete the synchronization of the NFT program from Solana Devnet to Grid and then attempt the mint operation again (as detailed in Step 2), we will see a successful minting message. This confirms that the NFT program deployed on Solana Devnet has been successfully synchronized to Grid and is operating correctly.

We can open the Backpack wallet again and switch the RPC to https://rpc.hypergrid.dev/. In the wallet’s Collectibles section, we can see this NFT token.

Modifying NFTs on the base layer through Hypergrid

We will attempt to mint a token using an NFT program that has been deployed on the Solana Devnet, then switch to the Hypergrid network to modify the token directly on Hypergrid, and verify the modification results.

Note: To ensure the security of NFT data, we need special locking mechanisms, so our NFT must support lock and unlock functionalities. We have deployed an NFT program with lock and unlock features on the Solana Devnet: 4WTUyXNcf6QCEj76b3aRDLPewkPGkXFZkkyf3A3vua1z.

1

Step 1:

We first access the specified NFT on the Solana Devnet and complete the minting process, creating an NFT token with a locking and unlocking mechanism.The related TypeScript code is as follows:

async function mintNft(metadata) {
if (isLoading) return;
setIsLoading(true);
try {
  const program = new anchor.Program(hgnft as anchor.Idl, "4WTUyXNcf6QCEj76b3aRDLPewkPGkXFZkkyf3A3vua1z");

  const tx = await program.methods
    .mintnft(metadata.name, metadata.uri, new BN(metadata.level))
    .accounts({ mint: newAccount.publicKey })
    .signers([newAccount])
    .rpc();

  const txhash = `https://explorer.solana.com/tx/${tx}?cluster=devnet`;
  console.log(`mint nft tx: `, txhash);
  setMintNftTX(txhash);

  const newMint = await program.account.mint.fetch(newAccount.publicKey);
  console.log('newMint', newMint);
  const metadata_ = {
    ...metadata,
    name: newMint.name.toString(),
    uri: newMint.uri.toString(),
    level: Number(newMint.level),
    locker: newMint.locker.toString()
  };
  setMetadata(metadata_);

  setIsLoading(false);
  openMintSuccess();
} catch (error) {
  console.error(error);
  setIsLoading(false);
  toast({ title: 'Mint nft failed', status: 'error' });
  }
}

For more detailed source code, please visit:
https://github.com/mirrorworld-universe/hypergrid-integrated-demo/blob/main/App/sonic-mint-demo/pages/write/index.tsx#L172

2

Step 2:

Next, we will switch to Grid and send a request to the Solana devnet to fetch the NFT onto Grid. Then, we will issue a modification command to adjust the NFT’s Level value. This process will trigger the locker and setvalue methods of the NFT to ensure the consistency and integrity of the transaction. The relevant TypeScript code is as follows:

 async function upgradeRequest() {
if (isLoading) return;
setIsLoading(true);
try {
  const transaction = new Transaction();
  const instruction1 = new TransactionInstruction({
    keys: [
      { pubkey: new PublicKey("4WTUyXNcf6QCEj76b3aRDLPewkPGkXFZkkyf3A3vua1z"), isSigner: false, isWritable: false },
      { pubkey: new PublicKey(newAccount.publicKey), isSigner: false, isWritable: true }
    ],
    programId: new PublicKey("SonicAccountMigrater11111111111111111111111"),
    data: createInstructionData(0)
  });
  const program = new anchor.Program(hgnft as anchor.Idl, "4WTUyXNcf6QCEj76b3aRDLPewkPGkXFZkkyf3A3vua1z");
  const instruction2 = await program.methods
    .fakesetvalue(new BN(metadata.level + 1))
    .accounts({ mint: newAccount.publicKey })
    .instruction();
  transaction.add(instruction1);
  transaction.add(instruction2);
  let provider: any = anchor.getProvider();
  const tx = await provider.sendAndConfirm(transaction);
  const txhash = `https://explorer.hypergrid.dev/tx/${tx}?cluster=custom&customUrl=https://rpc.hypergrid.dev`;
  console.log(`L2SetValueTX`, txhash);
  setL2SetValueTX(txhash);
  const transactionRes = await getTransaction(tx);
  if (transactionRes) {
    const logMessages = transactionRes.meta.logMessages;
    const targetPrefix = 'Sonic BaseLayer Transaction Signature: ';
    const targetMessage = logMessages.find((message) => message.startsWith(targetPrefix));
    if (targetMessage) {
      const tx = targetMessage.slice(targetPrefix.length);
      const txhash = `https://explorer.solana.com/tx/${tx}?cluster=devnet`;
      console.log('L1SetValueTX', txhash);
      setL1SetValueTX(txhash);
    }
  }
  setIsLoading(false);
  openUpgradeSuccess();
} catch (error) {
  console.error(error);
  setIsLoading(false);
  toast({ title: 'Upgrade failed', status: 'error' });
  }
}

For more detailed source code, please visit:
https://github.com/mirrorworld-universe/hypergrid-integrated-demo/blob/main/App/sonic-mint-demo/pages/write/index.tsx#L213

3

Step 3:

Finally, we will sync the latest modified NFT back to Grid from the Solana Devnet. The relevant TypeScript code is as follows:

const newMint = await program.account.mint.fetch(newAccount.publicKey);
console.log('newMint', newMint);
const metadata_ = {
  ...metadata,
  name: newMint.name.toString(),
  uri: newMint.uri.toString(),
  level: Number(newMint.level),
  locker: newMint.locker.toString()
};
setMetadata(metadata_);

For more detailed source code, please visit:
https://github.com/mirrorworld-universe/hypergrid-integrated-demo/blob/main/App/sonic-mint-demo/pages/write/index.tsx#L193

You can open the Backpack wallet, switch the RPC to: https://rpc.hypergrid.dev, and you will be able to see the latest NFT information in the Collectibles section.