In this article, we will mint our NFT using , which is a Solana program framework. Learn more about anchor in this . anchor link Table of contents Environmental Setup Interacting With the Token Program Interacting With Metaplex Metadata Program Calling Our Program from TS client when reading this article a or comment means that the code has been cut. Note: // snip # snip 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 using cargo, avm 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 to initialize our new anchor project. anchor cli 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 the feature for anchor-lang crate. This feature allows us to create an account if it isn't created automatically for us. init_if_needed cargo add anchor-lang --features=init-if-needed We will also install the crate with features. The create provides us with almost all the necessary functionality required to interact with the Token Program while the feature brings into scope the functionality required to interact the Metaplex's Token Metadata Program. anchor-spl metadata anchor-spl metadata cargo add anchor-spl --features=metadata Running a quick , ensures that everything is working in tandem with each other and we are not dealing with dependency conflicts. anchor build We are now ready to start writing our anchor program to mint NFTs for us. Open the file and it should have contents similar to this programs/solana-nft-anchor/lib.rs use anchor_lang::prelude::*; declare_id!("9TEtkW972r8AVyRmQzgyMz8GpG7WJxJ2ZUVZnjFNJgWM"); // 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 , you can ignore it as it will go away as soon as we start working on our code. struct takes 0 lifetime arguments but lifetime argument was supplied expected 0 lifetime arguments We will rename to and bring into scope the accounts we'll interact with when creating our NFT in the struct. Initialize InitNFT Accounts 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 differentiate a signer account from a normal one? We mark it as one. There are two ways to do this, using the account variant or by using anchor constraints which is what we are doing. Anchor constraints defined using are built-in features to simplify common security checks, e.g. mark accounts as mutable or not. parallel program execution layer(Sealevel) Signer #[account(<constraints>)] 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 . This is vital when using the wrapper. We will discuss this after introducing the wrapper in the next section. /// CHECK: ok, we are passing in this account ourselves AccountInfo Account 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 all the accounts which you'll interact with. explicitly declare This brings us to the second account which we bring into scope, the account. The mint account of a token contains details about the token such as mint authority, freeze authority, total supply ...etc. Mint 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 account. Let's go over them. The first constraint is like "a wrapper" around the 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 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 sets the decimals of our NFT token. You can't have a 0.25 NFT! Finally, we set the field to our address. mint init system_instruction::create_account() payer = signer Read more here mint::decimals = 0 mint::authority = signer.key(), mint::freeze_authority = signer.key(), Looking at this newly added account, the declaration is different from the first account. We are now using the account type instead of . Account AccountInfo The anchor 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 . AccountInfo /// CHECK: <comment explaining why are we blindly trusting this account> The account type is a more secure way of declaring your accounts. It contains all the methods of , but it verifies program ownership and deserializes underlying data into the specified type. Our above, Account checks that the owner of the our is indeed the Token program and that the account contains all the required fields for a Mint account. Account AccountInfo mint TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA The fields contained that the contains Mint account - 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) mint_authority : The total supply of tokens. In our case when minting an NFT, the supply is going to be set to 1. supply : 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. decimals : Has the token mint been initialized? True, when we call the mint instruction. is_initialized : Address allowed to freeze the token account. freeze_authority The mint does not hold any tokens. Note: If the mint account does not hold your tokens then where are they stored? When creating a token on Solana, you need the to hold your newly minted tokens. The token account contains fields such as Token account - The mint address associated with this account. mint - The address that has authority over this account. owner - The number of tokens in this account. amount - Address of another address that can manage your token account. That means, transferring or freezing your asset. delegate - The account state. It is an enum of three possible values, , for when the account does not exist, , for when the account has been created and exists, , for when the account has been frozen by the freeze authority. state Uninitialized Initialized Frozen , is this token the native Solana token. is_native - amount delegated to the field mentioned above. delegated_amount delegate - Address which can close this account. close_authority 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 is a PDA that is deterministically derived using the and account. Associated Token Account address mint 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 account is defined by the Account type. mint Mint We have an , with multiple anchor constraints. We use the 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 and as constraints, to link the mint to the token account. associated_token_account init_if_needed authority mint 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 field marked as true. Read more about the . executable Solana account model Anchor also provides another primitive to make working with Programs easier, We use 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 s won't match and the program executions will be halted. Program ID Let's talk about the four programs. - the Token program token_program - handles creation of our ATA(Associated Token Account). associated_token_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. system_program - 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. rent 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 as I did in my 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. invoke previous article Anchor simplifies this for us, by having a 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. CpiContext Let's take a look at how we can use the CpiContext to initialize a token mint. To initialize the , call the associative function which takes in two arguments. CpiContext new 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 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. invoke 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 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. CpiContext::new() 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!("9TEtkW972r8AVyRmQzgyMz8GpG7WJxJ2ZUVZnjFNJgWM"); // 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 monkey non-fungible without the monkey image? 😂. In the next section, we dive into using the Metaplex Token Metadata Program. Interacting With The 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 to add metadata to our spl-token. Token Metadata Program 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 . 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 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 . Program Derived Accounts find_program_address() found here The Metaplex Metadata Program also uses PDAs. Like the Associated Token Program, the Metadata Program uses PDAs for the that attaches itself to the Mint Account. The metadata account is derived using the following seeds, , Token Metadata Program pubkey i.e and finally the public key of the 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. metadata account metadata metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s mint 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. . 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 . Why is it needed. The token program accesses different accounts such as the for print editions of an NFT, the for programmable NFTs e.t.c. In our case for the, metadata account, this field is marked with the enum variant. key anchor discriminator edition account token record MetadataV1 Here is an exhaustive list of these markers and accounts and an explanation of what they are used for. : The account does not exist yet and needs to be created. Uninitialized This account holds the token metadata(the one we're currently working with). MetadataV1 The master edition account allows NFTs to be a limited or unlimited times amount of times. When we say printed, what we mean is making copies of the NFT MasterEditionV1 and MasterEditionV2 printed The edition account derived from a mint account represents an NFT that was copied from a Master Edition NFT. EditionV1 This account is used to internally keep track of which editions were printed and which ones have not. EditionMarker and EditionMarkerV2 used by programmable NFTs only. Token Record accounts enable us to attach custom data to token accounts rather than mint account. TokenRecord These accounts are used to store multiple delegate authorities for a given metadata account. MetadataDelegate Keeps track of which authorities are allowed to set and/or verify the collection of the metadata account. CollectionAuthorityRecord Keeps track of which authorities are allowed to reduce the uses( ) field on the metadata account. UseAuthorityRecord might be deprecated soon An escrow account that is managed by the holder of the NFT. TokenOwnedEscrow and (DEPRECATED) Used for reservation lists. if present on the list you can get an edition number given by your position on the list. ReservationListV1 ReservationListV2 - address allowed to make changes to the metadata account. update_authority - address of the mint account. mint - the asset data for with the token. This includes values such as the , , , and the data name symbol uri creator royalties creators - indicates whether the token has already been sold at least once. primary_sale_happened - indicates whether the metadata account can ever be changed. is_mutable - nonce used to verify the edition nonce's of printed NFTs. edition_nonce - the type of token that the metadata account holds. It is an optional enum that consists of the following variants token_standard - A non-fungible with a master edition. NonFungible - An spl token with a supply > 1, but which has NFT attributes such as an image and an attributes array. FungibleAsset (semi fungible) - A token with metadata and supply > 1. Fungible - Special type of NonFungible that's frozen al times and enforces custom authorization rules. ProgrammableNonFungible - an optional struct that contains the public key of a collectionNft or None if not present. collection - 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. uses There's a soon. proposal to deprecate it - An optional field with a V1 field that contains the number of NFTs for an NFT collection. collection_details It's deprecated and might be removed soon - 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. programmable_config Enough talk, let's Buidl!!!! In our struct we will bring into scope the account we need to add metadata to our spl-token. These are, to store our metadata, 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. InitNFT metadata_account master_edition_account To put the above paragraphs into code, we first install a version crate. mpl-token-metadata 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 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 explaining why it is untyped. You might be wondering why we did not use the typed metadata and master edition account. That is because anchor expects those account types to be initialized beforehand. address="" /// CHECK: <reason why the account is untyped> Account<'info, MetadataAccount> Account<'info, MasterEditionAccount> After bringing the needed accounts and program into scope, we now need to instantiate them. The with crate comes with a useful number of Cross Program Invocation(CPI) instructions we can use. anchor-spl metadata As we did with the mint account we will be using the method to help us make sure we have all the required accounts when making the CPI call to the . CpiContext::new() metadata_program We will use the to create the metadata account. It takes in five arguments, create_metadata_accounts_v3() 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 struct need to take in the asset data, we have changed the definition of our function to also include the , and parameter which we are passing as our asset data. This is also the same way we would get the other fields in the struct such as the , if there were more than one ... e.t.c. DataV2 init_nft name symbol uri DataV2 seller_fee_basis_points creators Finally we finish off our program by creating the master edition account, by invoking the 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. create_master_edition_v3 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!("9TEtkW972r8AVyRmQzgyMz8GpG7WJxJ2ZUVZnjFNJgWM"); // 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 , to deploy to Devnet when we run the deploy command. Anchor.toml To do this we will change the point to cluster 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 message. Deploy success Calling Our Program from TS client For our client, we will be using from Metaplex. Umi is a "Solana Framework for JavaScript clients". umi Let's install the packages we will be using. Umi will help us in deriving the PDAs for the metadata account and master edition while spl token will help us when deriving the associated token account. yarn add @solana/spl-token @metaplex-foundation/mpl-token-metadata @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/umi-signer-wallet-adapters Once installed, we will go ahead and create a new Umi instance using the function and register our local provider wallet with the token metadata program with Umi's interfaces. createUmi // snip const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace.SolanaNftAnchor as Program<SolanaNftAnchor>; const signer = provider.wallet; const umi = createUmi("https://api.devnet.solana.com") .use(walletAdapterIdentity(signer)) .use(mplTokenMetadata()); //snip With our config set up, let's work on calling our method. We will need to derive our associated token account, metadata account and master edition. Operations are made easy with the helper functions from the packages we just installed. init_nft the PublicKey interface is not compatible with umi's publicKey interface so be sure to wrap the public key values with it should an umi function require it as input. Note: @solana/web3.js // generate the mint account const mint = anchor.web3.Keypair.generate(); // Derive the associated token address account for the mint const associatedTokenAccount = await getAssociatedTokenAddress( mint.publicKey, signer.publicKey ); // derive the metadata account let metadataAccount = findMetadataPda(umi, { mint: publicKey(mint.publicKey), })[0]; //derive the master edition pda let masterEditionAccount = findMasterEditionPda(umi, { mint: publicKey(mint.publicKey), })[0]; Once we have derived our public keys, we will also need the asset data, i.e. the name, symbol and uri. We will not do metadata upload in this tutorial but in a separate UMI guide later. Right now we will utilize the data that I used for the previous tutorial. import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { SolanaNftAnchor } from "../target/types/solana_nft_anchor"; import { walletAdapterIdentity } from "@metaplex-foundation/umi-signer-wallet-adapters"; import { getAssociatedTokenAddress } from "@solana/spl-token"; import { findMasterEditionPda, findMetadataPda, mplTokenMetadata, MPL_TOKEN_METADATA_PROGRAM_ID, } from "@metaplex-foundation/mpl-token-metadata"; import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; import { publicKey } from "@metaplex-foundation/umi"; import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, } from "@solana/spl-token"; describe("solana-nft-anchor", async () => { // Configured the client to use the devnet cluster. const provider = anchor.AnchorProvider.env(); anchor.setProvider(provider); const program = anchor.workspace .SolanaNftAnchor as Program<SolanaNftAnchor>; const signer = provider.wallet; const umi = createUmi("https://api.devnet.solana.com") .use(walletAdapterIdentity(signer)) .use(mplTokenMetadata()); const mint = anchor.web3.Keypair.generate(); // Derive the associated token address account for the mint const associatedTokenAccount = await getAssociatedTokenAddress( mint.publicKey, signer.publicKey ); // derive the metadata account let metadataAccount = findMetadataPda(umi, { mint: publicKey(mint.publicKey), })[0]; //derive the master edition pda let masterEditionAccount = findMasterEditionPda(umi, { mint: publicKey(mint.publicKey), })[0]; const metadata = { name: "Kobeni", symbol: "kBN", uri: "https://raw.githubusercontent.com/687c/solana-nft-native-client/main/metadata.json", }; it("mints nft!", async () => { const tx = await program.methods .initNft(metadata.name, metadata.symbol, metadata.uri) .accounts({ signer: provider.publicKey, mint: mint.publicKey, associatedTokenAccount, metadataAccount, masterEditionAccount, tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID, systemProgram: anchor.web3.SystemProgram.programId, rent: anchor.web3.SYSVAR_RENT_PUBKEY, }) .signers([mint]) .rpc(); console.log( `mint nft tx: https://explorer.solana.com/tx/${tx}?cluster=devnet` ); console.log( `minted nft: https://explorer.solana.com/address/${mint.publicKey}?cluster=devnet` ); }); }); Let's go ahead and run the test to mint our NFT. anchor test Running will build and deploy your program first before running the ts client. anchor test To skip this process you can add the and flags. I prefer to use this since I already built and deployed the program at the previous stage. --skip-build --skip-deploy anchor test --skip-build --skip-deploy After running the command above, you should be greeted with your newly minted NFT tx. Clicking on the transaction link to the explorer, under the Token balances, you should have a change of . +1 To view the minted NFT, open the link and you should have something similar to this. minted nft Also published . here