How to mint NFTs on Solana with Rust
Let's dive a bit into the NFTs world, and learn how to mint one from a Smart Contract / Program on Solana
During my journey as a Solana developer, I always found myself banging my head against the wall. One of those occasions was while trying to mint a NFT directly from a Rust Smart Contract / Program.
Hopefully, this post will save some headaches to some of you..
Please note that the values expressed here (such as creators shares or seller fees basis points, are just placeholders to be replaced as needed)
There are some accounts that we will have to take into consideration in order to mint our NFT:
Mint Account
Metadata Account
Master Edition Account
The Token Metadata accounts detailed explanation can be found in here, but let’s have a quick overview of it.
Mint Account
A mint account refers to a type of account that is responsible for creating new tokens. That account will hold information such as the mint authority, freeze authority, token decimals..
Metadata Account
The Metadata Account is responsible for storing additional data attached to tokens. As every account in the Token Metadata program, it derives from the Mint Account of the token using a PDA (Program Derived Address).
The Metadata Account is derived from the seeds “metadata”, the Token Metadata program ID and the Mint itself.
The URI field points to an off-chain JSON file containing more data
Master Edition Account
The Master Edition accounts is derived from the Mint Account
When creating a Master Edition account, the Token Metadata program will check for the following conditions:
The Mint Account has zero decimals, i.e.
Decimals = 0
.The Mint Account minted exactly one token to a wallet, i.e.
Supply = 1
.
Additionally, it will transfer the Mint Authority
and the Freeze Authority
to the Master Edition account to prevent anyone from being able to mint additional tokens.
All this mentioned checks proves the Non-Fungibility of the token
The Master Edition Account is derived from the seeds “metadata”, the Token Metadata program ID the Mint itself and “edition”.
Summing all up,
the Metadata Account will be an account associated (derived) to our Mint Account and will contain any metadata that is relevant to our mint, however it will not prove the Non-Fungibility aspect of the token.
That part will be taken care by the Master Edition account (also derived from our Mint Account) that will perform the needed checks and prevent any more tokens from being minted.
The Program (Smart Contract) Part
For this part we will assume that you already have the proper tools installed (Solana CLI, Anchor Framework...).
By the time this was written, the dependencies in use (in the Cargo.toml file) were the following:
[dependencies]
anchor-lang = "=0.27.0"
anchor-spl = "=0.27.0"
solana-program = "=1.14.16"
mpl-token-metadata = {version = "1.10.0", features = ["no-entrypoint"]}
The Mint NFT Struct
Lets start by creating our structure which will be responsible for “holding” the accounts for our mint instruction (consider creating it in a different file from your lib.rs file for the sake of readability):
#[derive(Accounts)]
pub struct MintNFT<'info> {
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub mint: AccountInfo<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub token_account: AccountInfo<'info>,
pub mint_authority: Signer<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub metadata: AccountInfo<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
#[account(mut)]
pub master_edition: AccountInfo<'info>,
#[account(mut)]
pub payer: Signer<'info>,
/// CHECK: This is not dangerous because we don't read or write from this account
pub rent: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
/// CHECK: This is not dangerous because we don't read or write from this account
pub token_metadata_program: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
Lets brake down our implementation of the mint_nft operation:
Minting one token
We achieve this by creating a CPI (Cross Program Invocation) to mint a token between our program and the Token Program
impl<'info> MintNFT<'info> {
pub fn mint_nft(&mut self, creator_key: Pubkey, uri: String, title: String) -> Result<()> {
let cpi_program = self.token_program.to_account_info();
msg!("CPI Program Created");
let cpi_accounts = MintTo {
mint: self.mint.to_account_info(),
to: self.token_account.to_account_info(),
authority: self.payer.to_account_info(),
};
msg!("CPI Accounts Created");
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
msg!("CPI Context Created");
token::mint_to(cpi_ctx, 1)?;
msg!("Token Minted!");
Creating the metadata account for the mint
We achieve this by invoking Create Metadata Accounts V3 from Metaplex
let account_info = vec![
self.metadata.to_account_info(),
self.mint.to_account_info(),
self.mint_authority.to_account_info(),
self.payer.to_account_info(),
self.token_metadata_program.to_account_info(),
self.token_program.to_account_info(),
self.system_program.to_account_info(),
self.rent.to_account_info(),
];
msg!("Account Info Vector Created!");
let creator = vec![
mpl_token_metadata::state::Creator {
address: creator_key,
verified: false,
share: 100,
},
];
msg!("Creator vector created)!;
let symbol = std::string::ToString::to_string("STU");
invoke(
&create_metadata_accounts_v3(
self.token_metadata_program.key(), //program_id
self.metadata.key(), //metadata_account
self.mint.key(), //mint
self.mint_authority.key(), //mint_authority
self.payer.key(), //payer
self.payer.key(), //update_authority
title, //name
symbol, //symbol
uri, //uri
Some(creator), //creators
500, //seller_fee_basis_points
true, //update_authority_is_signer
false, //is_mutable
None, //collection
None, //uses
None, //collection_details
),
account_info.as_slice(),
)?;
msg!("Metadata Account Created!");
Creating the Master Edition Account:
We achieve this by invoking Create Master Edition V3 from Metaplex
let master_edition_infos = vec![
self.master_edition.to_account_info(),
self.mint.to_account_info(),
self.mint_authority.to_account_info(),
self.payer.to_account_info(),
self.metadata.to_account_info(),
self.token_metadata_program.to_account_info(),
self.token_program.to_account_info(),
self.system_program.to_account_info(),
self.rent.to_account_info(),
];
msg!("Master Edition Account Infos Created");
invoke(
&create_master_edition_v3(
self.token_metadata_program.key(), //program_id
self.master_edition.key(), //edition
self.mint.key(), //mint
self.payer.key(), //update_authority
self.mint_authority.key(), //mint_authority
self.metadata.key(), //metadata (metadata_account)
self.payer.key(), //payer
Some(0), //max_supply -> Total (1) minus Supply (1)
),
master_edition_infos.as_slice(),
)?;
msg!("Master Edition Account Created!");
Ok(())
}
}
Calling the Mint Function from your lib.rs file
We go ahead and create our method in our lib.rs file
pub fn mint_nft(ctx: Context<MintNFT>, creator_key: Pubkey, uri: String, title: String) -> Result<()> {
ctx.accounts.mint_nft(creator_key, uri, title)?;
msg!("NFT Successfully minted!");
Ok(())
}
Testing our Program
In order to test our program, there are some helper functions that we will add to our anchor test file (/tests/’TEST_FILE.ts’).
These functions will fetch the Metadata and Master Edition account for our mint (remember the seeds to derive it?)
const TOKEN_METADATA_PROGRAM_ID = new anchor.web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
const getMetadata = async (mint: anchor.web3.PublicKey): Promise<anchor.web3.PublicKey> => {
return (
anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
)
)[0];
};
const getMasterEdition = async (mint: anchor.web3.PublicKey): Promise<anchor.web3.PublicKey> => {
return (
anchor.web3.PublicKey.findProgramAddressSync(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
mint.toBuffer(),
Buffer.from("edition"),
],
TOKEN_METADATA_PROGRAM_ID
)
)[0];
};
Lets now create a new Mint Account, Create the ATA (Associated Token Account) and fetch the Metadata and Master Edition accounts:
“keypair” will be the payer of mint account initialization fees and “receiver” will be both the mint authority and freeze authority (In this case, receiver is our “provider”).
You might need to create “keypair” and transfer some SOL, or import it from a file.
“receiver” will be the owner of the Associated Token Account and “keypair” will pay for the account initialization fees. In this example, we are using our provider as receiver
let receiver = provider.wallet.publicKey;
console.log("User: ", receiver.toString());
let NFTmintKey = await createMint(connection, keypair, receiver, receiver, 0, NFTKeypair);
console.log("Mint created! : ", NFTmintKey.toString());
let associatedTokenAccount = await getOrCreateAssociatedTokenAccount(connection, keypair, NFTmintKey, receiver);
console.log("ATA Account created: ", associatedTokenAccount.address);
console.log(await connection.getParsedAccountInfo(NFTmintKey));
const metadataAddress = await getMetadata(NFTmintKey);
const masterEdition = await getMasterEdition(NFTmintKey);
Another solution to avoid having the “keypair”, would be to create a transaction to create and initialize the mint account and send it through the provider.
This part is optional and replaces the “createMint” and “getOrCreateAssociatedTokenAccount” instructions:
const mint_nft_tx = new anchor.web3.Transaction().add(
anchor.web3.SystemProgram.createAccount({
fromPubkey: receiver,
newAccountPubkey: NFTmintKey.publicKey,
space: MINT_SIZE,
programId: TOKEN_PROGRAM_ID,
lamports,
}),
createInitializeMintInstruction(NFTmintKey.publicKey, 0, receiver, receiver),
createAssociatedTokenAccountInstruction(receiver, associatedTokenAccount, receiver, NFTmintKey.publicKey)
);
const res = await provider.sendAndConfirm(mint_nft_tx, [NFTmintKey]);
We will now execute our contract to mint our NFT into our specified ATA
const tx = await program.methods.mintNft(
new anchor.web3.PublicKey("YOUR_CREATOR"),
"https://arweave.net/'YOUR_URI'",
"Superteam UAE").accounts(
{
mintAuthority: receiver,
mint: NFTmintKey.publicKey,
tokenProgram: TOKEN_PROGRAM_ID,
metadata: metadataAddress,
tokenAccount: associatedTokenAccount,
tokenMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
payer: receiver,
systemProgram: anchor.web3.SystemProgram.programId,
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
masterEdition: masterEdition,
},
).rpc();
console.log("Your transaction signature", tx);
console.log("NFT Mint Operation Finished!");
});
In case you’re not using your provider as receiver, don’t forget to add it as a signer of your transaction.
And voilà… Congrats!! You just minted an NFT from your Rust Smart Contract!
You can go ahead and check the transaction signature in SOL Explorer (Don’t forget to choose your cluster accordingly)
A GitHub repo with the described example can be found here.