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.
Install avm using cargo,
avm to install anchor cli
anchor --version && solana --version
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.
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
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.
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
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.
- 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.
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.
- token_program - the Token program
- associated_token_program - handles creation of our ATA(Associated Token Account).
- 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.
- 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.
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.
- The external program we are cpi-ing into.
- 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.
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.
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 theirToken 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.
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.
- 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.
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.
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.
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.
Anchor.toml, to deploy to Devnet when we run the deploy command.
To do this we will change the cluster point to devnet
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.




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 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#L129Sure, this mint operation will fail because the NFT program (
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:https://github.com/mirrorworld-universe/hypergrid-integrated-demo/blob/main/App/sonic-mint-demo/pages/read/index.tsx#L129Sure, 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 (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
HdBvhzMrhmdPyrbwL9ZR2ZFqhqVSKcDra7ggdWqCcwps) from the Solana Devnet to the Grid cache, and then try the mint operation again. Here is a snippet of the TypeScript code: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: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
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: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
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: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#L193You 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.
https://github.com/mirrorworld-universe/hypergrid-integrated-demo/blob/main/App/sonic-mint-demo/pages/write/index.tsx#L193You 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.
