Non Fungible Token Metadata

Non Fungible Token Metadata

Overview

In Ethereum, ERC20 protocol is used to manage fungible token, and ERC721 is used to manager non fungible token. Their data structure is different.
  • In ERC20, the ownership data is recorded using mapping(address userAddress ⇒ uint256 balance), each user can have many fungible tokens.
  • In ERC721, the ownership data is recorded using mapping(uint256 nftId ⇒ address userAddress), each NFT has unique ID, and can only have one owner.
 
In solana, fungible token and non fungible token are managed using same protocol called SPL Token Program. There is a MINT account records the mint_authority, supply information of the token. And there are associated token accounts (ATA) record users’ token balances.
To deploy NFT, we need to deploy two MINT accounts. One is for collection, the other is for NFT. And both should have supply 1, so that assets are non-fungible. Collection and individual NFT have their own metadata information like name, image, etc, so we need to deploy metadata acocunt for both.
Metaplex protocol suppports to deploy and manage metadata account for both collection and NFT mint account. Metadata accounts for collection and NFT mint are pdas derived from and owned by metaplex program, and they have same data structure.
notion image
 

Metadata

Fields:
  1. key:
    1. This field identifies the type of account. The Key enum distinguishes between different types of metadata and related accounts within the Metaplex program,
  1. update_authority
    1. The public key (Pubkey) that has the authority to update the metadata. This authority can change the metadata fields, as long as the is_mutable flag allows it.
  1. mint
    1. public key of the mint account(collection or NFT mint account).
  1. data
    1. encapsulates the core attributes of the NFT, including:
      • name: The name of the collection or NFT.
      • symbol: The symbol or ticker.
      • uri: A URI pointing to a JSON file that describes the collection or NFT's metadata (e.g., image, description, attributes).
      • seller_fee_basis_points: The percentage of secondary sales that go to the creators, expressed in basis points (1% = 100 basis points).
      • creators: An optional array of Creator structs that lists the creators of the NFT, potentially with share allocations.
  1. primary_sale_happened
    1. A flag indicating whether the NFT has been sold in a primary sale. Once set to true, any subsequent sales are considered secondary sales.
  1. is_mutable
    1. This flag determines whether the metadata can be updated. If false, the metadata becomes immutable and cannot be changed.
  1. edition_nonce
    1. This optional field is used to facilitate the creation of editions of the NFT. It helps in generating unique keys for editions. We will introduce edition NFT later.
  1. token_standard
    1. specifies the type of token:
      • NonFungible: A master edition (unique, non-replicable NFT).
      • FungibleAsset: A token with attributes but not unique (e.g., semi-fungible tokens).
      • Fungible: A standard fungible token with simple metadata.
      • NonFungibleEdition: A limited edition of a non-fungible token.
      • ProgrammableNonFungible and ProgrammableNonFungibleEdition: NFTs with programmable configurations that allow additional functionality.
  1. collection
    1. The Collection struct links the NFT to a collection, indicating whether the collection has been verified and providing the collection’s public key.
  1. uses
    1. struct defines how the NFT can be used, including:
      • use_method: UseMethod: The method of use.
      • remaining: u64: The number of uses left.
      • total: u64: The total number of uses available.
  1. collection_details
    1. The CollectionDetails enum is used to distinguish collection MINT from NFT MINT. For collection MINT, this field is set to be V1. For NFT MINT, this field is set to be None.
  1. programmable_config
      • The ProgrammableConfig enum allows for the inclusion of programmable rules, such as authorization rules that can be associated with the NFT.
/// --- programs/token-metadata/program/src/state/metadata.rs --- pub struct Metadata { /// Account discriminator. pub key: Key, /// Address of the update authority. #[cfg_attr(feature = "serde-feature", serde(with = "As::<DisplayFromStr>"))] pub update_authority: Pubkey, /// Address of the mint. #[cfg_attr(feature = "serde-feature", serde(with = "As::<DisplayFromStr>"))] pub mint: Pubkey, /// Asset data. pub data: Data, // Immutable, once flipped, all sales of this metadata are considered secondary. pub primary_sale_happened: bool, // Whether or not the data struct is mutable, default is not pub is_mutable: bool, /// nonce for easy calculation of editions, if present pub edition_nonce: Option<u8>, /// Since we cannot easily change Metadata, we add the new DataV2 fields here at the end. pub token_standard: Option<TokenStandard>, /// Collection pub collection: Option<Collection>, /// Uses pub uses: Option<Uses>, /// Collection Details pub collection_details: Option<CollectionDetails>, /// Programmable Config pub programmable_config: Option<ProgrammableConfig>, } pub enum Key { Uninitialized, EditionV1, MasterEditionV1, ReservationListV1, MetadataV1, ReservationListV2, MasterEditionV2, EditionMarker, UseAuthorityRecord, CollectionAuthorityRecord, TokenOwnedEscrow, TokenRecord, MetadataDelegate, EditionMarkerV2, HolderDelegate, } pub struct Data { /// The name of the asset pub name: String, /// The symbol for the asset pub symbol: String, /// URI pointing to JSON representing the asset pub uri: String, /// Royalty basis points that goes to creators in secondary sales (0-10000) pub seller_fee_basis_points: u16, /// Array of creators, optional pub creators: Option<Vec<Creator>>, } pub enum TokenStandard { NonFungible, // This is a master edition FungibleAsset, // A token with metadata that can also have attributes Fungible, // A token with simple metadata NonFungibleEdition, // This is a limited edition ProgrammableNonFungible, // NonFungible with programmable configuration ProgrammableNonFungibleEdition, // NonFungible with programmable configuration } pub enum ProgrammableConfig { V1 { /// Programmable authorization rules. #[cfg_attr( feature = "serde-feature", serde( deserialize_with = "deser_option_pubkey", serialize_with = "ser_option_pubkey" ) )] rule_set: Option<Pubkey>, }, } /// --- programs/token-metadata/program/src/state/collection.rs --- pub struct Collection { pub verified: bool, #[cfg_attr(feature = "serde-feature", serde(with = "As::<DisplayFromStr>"))] pub key: Pubkey, } pub enum CollectionDetails { #[deprecated( since = "1.13.1", note = "The collection size tracking feature is deprecated and will soon be removed." )] V1 { size: u64, }, V2 { padding: [u8; 8], }, } /// --- programs/token-metadata/program/src/state/uses.rs --- pub enum UseMethod { Burn, Multiple, Single, } pub struct Uses { // 17 bytes + Option byte pub use_method: UseMethod, //1 pub remaining: u64, //8 pub total: u64, //8 }

Tx & Address

Tx

Action
Hash
Create collection
Create nft
Collection creation tx contains two instructions:
  • MetadataInstruction:Create : create a NFT collection.
  • MetadataInstruction:Mint : mint collection token to collection creator.
 
NFT creation tx contains same two instruction with arguments difference.

Address

Name
Address
Collection
39obDjVzcxcK6fDn4BuBYa2Y1rC8gJnedVJZVVD1DGzC
NFT
8uuom3AyYQbi1MSDL8z1yDjXJ7G3dTPhHZMiSzdmZBsk
Collection Metadata Account
Gv4ujHD7DLuYsw1eXZ5AG2dnKsgSfk14EvgRx3gM3Lw3
NFT Metadata Account
GUGQNh8hMhbHBEHhsP69FqvAMpDEKdtqJbX2qBbim3We

Creat Collection # Create Collection

Metaplex uses MetadataInstruction:Create instruction to create a NFT collection.

Instruction Create

/// --- program/src/instruction/mod.rs --- #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] /// Instructions supported by the Metadata program. #[derive(BorshSerialize, BorshDeserialize, Clone, ShankInstruction, AccountContext)] #[rustfmt::skip] pub enum MetadataInstruction { /// ... /// Creates the metadata and associated accounts for a new or existing mint account. /// /// This instruction will initialize a mint account if it does not exist and /// the mint key is a signer on the transaction. /// /// When creating a non-fungible assert, the `master_edition` needs to be specified. #[account(0, writable, name="metadata", desc="Unallocated metadata account with address as pda of ['metadata', program id, mint id]")] #[account(1, optional, writable, name="master_edition", desc="Unallocated edition account with address as pda of ['metadata', program id, mint, 'edition']")] #[account(2, writable, name="mint", desc="Mint of token asset")] #[account(3, signer, name="authority", desc="Mint authority")] #[account(4, signer, writable, name="payer", desc="Payer")] #[account(5, name="update_authority", desc="Update authority for the metadata account")] #[account(6, name="system_program", desc="System program")] #[account(7, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(8, optional, name="spl_token_program", desc="SPL Token program")] #[args(initialize_mint: bool)] #[args(update_authority_as_signer: bool)] Create(CreateArgs), /// ... }

Instruction Args

/// --- program/src/instruction/metadata.rs --- #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum CreateArgs { V1 { asset_data: AssetData, decimals: Option<u8>, print_supply: Option<PrintSupply>, }, } #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] /// Represents the print supply of a non-fungible asset. pub enum PrintSupply { /// The asset does not have any prints. Zero, /// The asset has a limited amount of prints. Limited(u64), /// The asset has an unlimited amount of prints. Unlimited, } /// --- program/src/state/asset_data.rs --- /// Data representation of an asset. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub struct AssetData { /// The name of the asset. pub name: String, /// The symbol for the asset. pub symbol: String, /// URI pointing to JSON representing the asset. pub uri: String, /// Royalty basis points that goes to creators in secondary sales (0-10000). pub seller_fee_basis_points: u16, /// Array of creators. pub creators: Option<Vec<Creator>>, // Immutable, once flipped, all sales of this metadata are considered secondary. pub primary_sale_happened: bool, // Whether or not the data struct is mutable (default is not). pub is_mutable: bool, /// Type of the token. pub token_standard: TokenStandard, /// Collection information. pub collection: Option<Collection>, /// Uses information. pub uses: Option<Uses>, /// Collection item details. pub collection_details: Option<CollectionDetails>, /// Programmable rule set for the asset. #[cfg_attr( feature = "serde-feature", serde( deserialize_with = "deser_option_pubkey", serialize_with = "ser_option_pubkey" ) )] pub rule_set: Option<Pubkey>, }

Instruction Builder

/// --- program/src/instruction/metadata.rs --- /// Builds the instruction to create metadata and associated accounts. /// /// # Accounts: /// /// 0. `[writable]` Metadata account /// 1. `[optional, writable]` Master edition account /// 2. `[writable]` Mint account /// 3. `[signer]` Mint authority /// 4. `[signer]` Payer /// 5. `[signer]` Update authority /// 6. `[]` System program /// 7. `[]` Instructions sysvar account /// 8. `[]` SPL Token program impl InstructionBuilder for super::builders::Create { fn instruction(&self) -> solana_program::instruction::Instruction { let accounts = vec![ AccountMeta::new(self.metadata, false), // checks whether we have a master edition if let Some(master_edition) = self.master_edition { AccountMeta::new(master_edition, false) } else { AccountMeta::new_readonly(crate::ID, false) }, AccountMeta::new(self.mint, self.initialize_mint), AccountMeta::new_readonly(self.authority, true), AccountMeta::new(self.payer, true), AccountMeta::new_readonly(self.update_authority, self.update_authority_as_signer), AccountMeta::new_readonly(self.system_program, false), AccountMeta::new_readonly(self.sysvar_instructions, false), AccountMeta::new_readonly(self.spl_token_program.unwrap_or(SPL_TOKEN_ID), false), ]; Instruction { program_id: crate::ID, accounts, data: MetadataInstruction::Create(self.args.clone()) .try_to_vec() .unwrap(), } } }

Execute Instruction

Create::to_context extracts related accounts.
According to the type of the args, it decides to call corresponding handler function.Currently, there is only V1 type.
 
In the collection creation tx, we can see the createArgs:
{ "assetData": { "name": "Hello Solana NFT Collection", "symbol": "", "uri": "www.collectionUri.com", "sellerFeeBasisPoints": 0, "creators": [ { "address": "2FdSZMUKM8ZighMnurwNuboAFm8wAdqrERwimxtrXdR6", "verified": true, "share": 100 } ], "primarySaleHappened": false, "isMutable": true, "tokenStandard": { "enumType": "nonFungible" }, "collection": null, "uses": null, "collectionDetails": { "size": "0", "enumType": "v1" }, "ruleSet": null }, "decimals": 0, "printSupply": { "enumType": "zero" }, "enumType": "v1" }
 
/// --- program/src/processor/metadata/create.rs --- /// Create the associated metadata accounts for a mint. /// /// The instruction will also initialize the mint if the account does not /// exist. For `NonFungible` assets, a `master_edition` account is required. pub fn create<'a>( program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], args: CreateArgs, ) -> ProgramResult { let context = Create::to_context(accounts)?; match args { CreateArgs::V1 { .. } => create_v1(program_id, context, args), } } /// --- program/src/instruction/mod.rs --- pub struct Context<T> { pub accounts: T, // not currently in use //pub remaining_accounts: Vec<AccountInfo<'a>>, } /// --- program/src/instruction/metadata.rs --- #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum CreateArgs { V1 { asset_data: AssetData, decimals: Option<u8>, print_supply: Option<PrintSupply>, }, }
 
The create_v1 function is responsible for creating associated metadata accounts for a mint in the Solana blockchain, with special handling for non-fungible tokens (NFTs) and programmable non-fungible tokens (pNFTs). The function performs several critical tasks, such as validating and initializing mint accounts, creating metadata and master edition accounts, and setting programmable configurations.
Steps:
Parameters:
  • program_id: The public key of the program.
  • ctx: Context containing the accounts needed for the instruction.
  • args: Arguments for the create instruction.
Steps:
  1. Extract Arguments
    1. Extracts the arguments from CreateArgs which includes asset_data, decimals, and print_supply.
  1. Token Standard Validation
    1. Ensures that the token standard is not NonFungibleEdition or ProgrammableNonFungibleEdition as these are invalid for this instruction.
  1. Levy Fees
    1. Charges fees to fund the metadata account with rent and the protocol fee.
  1. Mint Account Initialization
      • Checks if the mint account is empty and initializes it if necessary.
      • Validates the mint account if it already exists, ensuring it conforms to the expected token standard properties (decimals, supply).
  1. Metadata Account Creation
    1. Creates the metadata account using the process_create_metadata_accounts_logic function, which takes various account infos and arguments related to the asset.
  1. Master Edition Account Creation
      • Creates the master edition account for non-fungible and programmable non-fungible tokens.
      • Ensures the master_edition account is provided and creates it using the create_master_edition function.
      • For pNFTs, stores the token standard value at the end of the master edition account data.
  1. Metadata State Update
      • Updates the metadata account with the token standard and primary sale status.
      • For pNFTs, sets the programmable configuration.
      • Saves the updated metadata state.
  1. Set Fee Flag
    1. Sets the fee flag after the metadata account is created to indicate that fees have been paid.
/// --- program/src/processor/metadata/create.rs --- /// V1 implementation of the create instruction. fn create_v1(program_id: &Pubkey, ctx: Context<Create>, args: CreateArgs) -> ProgramResult { /// Extract Arguments // get the args for the instruction let CreateArgs::V1 { ref asset_data, decimals, print_supply, } = args; /// Token Standard Validation // cannot create non-fungible editions on this instruction if matches!( asset_data.token_standard, TokenStandard::NonFungibleEdition | TokenStandard::ProgrammableNonFungibleEdition ) { return Err(MetadataError::InvalidTokenStandard.into()); } /// Levy Fees // Levy fees first, to fund the metadata account with rent + fee amount. levy(LevyArgs { payer_account_info: ctx.accounts.payer_info, token_metadata_pda_info: ctx.accounts.metadata_info, })?; /// Mint Account Initialization // if the account does not exist, we will allocate a new mint if ctx.accounts.mint_info.data_is_empty() { // mint account must be a signer in the transaction if !ctx.accounts.mint_info.is_signer { return Err(MetadataError::MintIsNotSigner.into()); } let spl_token_program = ctx .accounts .spl_token_program_info .ok_or(MetadataError::MissingSplTokenProgram)?; create_mint( ctx.accounts.mint_info, ctx.accounts.metadata_info, ctx.accounts.authority_info, ctx.accounts.payer_info, asset_data.token_standard, decimals, spl_token_program, )?; } else { let mint = validate_mint( ctx.accounts.mint_info, ctx.accounts.metadata_info, asset_data.token_standard, )?; if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible | TokenStandard::NonFungible ) { // NonFungible assets must have decimals == 0 and supply no greater than 1 if mint.decimals > 0 || mint.supply > 1 { return Err(MetadataError::InvalidMintForTokenStandard.into()); } // Programmable assets must have supply == 0 since there cannot be any // existing token account if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible ) && (mint.supply > 0) { return Err(MetadataError::MintSupplyMustBeZero.into()); } } } /// Metadata Account Creation // creates the metadata account process_create_metadata_accounts_logic( program_id, CreateMetadataAccountsLogicArgs { metadata_account_info: ctx.accounts.metadata_info, mint_info: ctx.accounts.mint_info, mint_authority_info: ctx.accounts.authority_info, payer_account_info: ctx.accounts.payer_info, update_authority_info: ctx.accounts.update_authority_info, system_account_info: ctx.accounts.system_program_info, }, asset_data.as_data_v2(), false, asset_data.is_mutable, false, true, asset_data.collection_details.clone(), None, None, )?; /// Master Edition Account Creation // creates the master edition account (only for NonFungible assets) if matches!( asset_data.token_standard, TokenStandard::NonFungible | TokenStandard::ProgrammableNonFungible ) { let print_supply = print_supply.ok_or(MetadataError::MissingPrintSupply)?; if let Some(master_edition) = ctx.accounts.master_edition_info { let spl_token_program = ctx .accounts .spl_token_program_info .ok_or(MetadataError::MissingSplTokenProgram)?; create_master_edition( program_id, master_edition, ctx.accounts.mint_info, ctx.accounts.update_authority_info, ctx.accounts.authority_info, ctx.accounts.payer_info, ctx.accounts.metadata_info, spl_token_program, ctx.accounts.system_program_info, print_supply.to_option(), )?; // for pNFTs, we store the token standard value at the end of the // master edition account if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible ) { let mut data = master_edition.data.borrow_mut(); if data.len() < MAX_MASTER_EDITION_LEN { return Err(MetadataError::InvalidMasterEditionAccountLength.into()); } data[TOKEN_STANDARD_INDEX] = TokenStandard::ProgrammableNonFungible as u8; } } else { return Err(MetadataError::MissingMasterEditionAccount.into()); } } else if print_supply.is_some() { msg!("Ignoring print supply for selected token standard"); } /// Metadata State Update let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; metadata.token_standard = Some(asset_data.token_standard); metadata.primary_sale_happened = asset_data.primary_sale_happened; // sets the programmable config for programmable assets if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible ) { metadata.programmable_config = Some(ProgrammableConfig::V1 { rule_set: asset_data.rule_set, }); } // saves the metadata state metadata.save(&mut ctx.accounts.metadata_info.try_borrow_mut_data()?)?; /// Set Fee Flag // Set fee flag after metadata account is created. set_fee_flag(ctx.accounts.metadata_info) }

Levy Fees

Transfer rent and protocol fee to the metadata account. Protocol fee can be withdrawed by the metaplex team.
/// --- program/src/processor/metadata/create.rs --- fn create_v1(program_id: &Pubkey, ctx: Context<Create>, args: CreateArgs) -> ProgramResult { /// ... // Levy fees first, to fund the metadata account with rent + fee amount. levy(LevyArgs { payer_account_info: ctx.accounts.payer_info, token_metadata_pda_info: ctx.accounts.metadata_info, })?; /// ... }
/// --- program/src/utils/fee.rs --- pub(crate) fn levy(args: LevyArgs) -> ProgramResult { // Fund metadata account with rent + Metaplex fee. let rent = Rent::get()?; let fee = CREATE_FEE + rent.minimum_balance(Metadata::size()); invoke( &solana_program::system_instruction::transfer( args.payer_account_info.key, args.token_metadata_pda_info.key, fee, ), &[ args.payer_account_info.clone(), args.token_metadata_pda_info.clone(), ], )?; Ok(()) } /// --- program/src/state/fee.rs --- // create_metadata_accounts_v3, create, print edition commands pub const CREATE_FEE: u64 = 10_000_000;

Mint Account Initialization

  • If the mint account mint_info ’s data is empty which means it hasn’t be created, then create it.
  • Else validate the mint account’s data.
    • For NonFungible token, the decimal should be 0 and supply should be 1.
    • For ProgrammableNonFungible token, the decimal and supply both should be 0.
/// --- program/src/processor/metadata/create.rs --- fn create_v1(program_id: &Pubkey, ctx: Context<Create>, args: CreateArgs) -> ProgramResult { /// ... // if the account does not exist, we will allocate a new mint if ctx.accounts.mint_info.data_is_empty() { // mint account must be a signer in the transaction if !ctx.accounts.mint_info.is_signer { return Err(MetadataError::MintIsNotSigner.into()); } let spl_token_program = ctx .accounts .spl_token_program_info .ok_or(MetadataError::MissingSplTokenProgram)?; create_mint( ctx.accounts.mint_info, ctx.accounts.metadata_info, ctx.accounts.authority_info, ctx.accounts.payer_info, asset_data.token_standard, decimals, spl_token_program, )?; } else { let mint = validate_mint( ctx.accounts.mint_info, ctx.accounts.metadata_info, asset_data.token_standard, )?; if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible | TokenStandard::NonFungible ) { // NonFungible assets must have decimals == 0 and supply no greater than 1 if mint.decimals > 0 || mint.supply > 1 { return Err(MetadataError::InvalidMintForTokenStandard.into()); } // Programmable assets must have supply == 0 since there cannot be any // existing token account if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible ) && (mint.supply > 0) { return Err(MetadataError::MintSupplyMustBeZero.into()); } } } /// ... }

Create Mint Account

create_mint is responsible for creating and initializing a mint account for a given token standard.
Parameters:
  • mint: The account info for the mint account to be created.
  • metadata: The account info for the metadata account.
  • authority: The account info for the authority that will control the mint.
  • payer: The account info for the payer who will cover the costs of creating the mint.
  • token_standard: The type of token to be created (e.g., fungible, non-fungible).
  • decimals: The number of decimals for the token, if applicable.
  • spl_token_program: The account info for the SPL Token program.
Steps:
  1. Check for SPL Token 2022
    1. Checks if the SPL Token program being used is SPL Token 2022.
  1. Calculate Mint Account Size
    1. Determines the size of the mint account based on whether SPL Token 2022 is used and includes the necessary extensions.
  1. Create Mint Account
    1. Invokes the system instruction to create the account, specifying the payer, the mint account, the required balance, the size, and the program ID.
  1. Initialize Extensions for SPL Token 2022
    1. If SPL Token 2022 is used, initializes the MintCloseAuthority and MetadataPointer extensions for the mint account.
  1. Set Decimals Based on Token Standard
    1. Determines the number of decimals based on the token standard. Non-fungible tokens always have 0 decimals, while fungible tokens use the provided decimals or a default value.
  1. Initialize the Mint Account
    1. Invokes the SPL Token program to initialize the mint account with the specified authority and decimals.
/// --- program/src/utils/token.rs --- /// Creates a mint account for the given token standard. /// /// When creating a mint with spl-token-2022, the following extensions are enabled: /// /// - mint close authority extension enabled and set to the metadata account /// - metadata pointer extension enabled and set to the metadata account pub(crate) fn create_mint<'a>( mint: &'a AccountInfo<'a>, metadata: &'a AccountInfo<'a>, authority: &'a AccountInfo<'a>, payer: &'a AccountInfo<'a>, token_standard: TokenStandard, decimals: Option<u8>, spl_token_program: &'a AccountInfo<'a>, ) -> ProgramResult { /// Check for SPL Token 2022 let spl_token_2022 = matches!(spl_token_program.key, &spl_token_2022::ID); /// Calculate Mint Account Size let mint_account_size = if spl_token_2022 { ExtensionType::try_calculate_account_len::<Mint>(&[ ExtensionType::MintCloseAuthority, ExtensionType::MetadataPointer, ])? } else { Mint::LEN }; /// Create Mint Account invoke( &system_instruction::create_account( payer.key, mint.key, Rent::get()?.minimum_balance(mint_account_size), mint_account_size as u64, spl_token_program.key, ), &[payer.clone(), mint.clone()], )?; /// Initialize Extensions for SPL Token 2022 if spl_token_2022 { let account_infos = vec![mint.clone(), metadata.clone(), spl_token_program.clone()]; invoke( &initialize_mint_close_authority(spl_token_program.key, mint.key, Some(metadata.key))?, &account_infos, )?; invoke( &metadata_pointer::instruction::initialize( spl_token_program.key, mint.key, None, Some(*metadata.key), )?, &account_infos, )?; } /// Set Decimals Based on Token Standard let decimals = match token_standard { // for NonFungible variants, we ignore the argument and // always use 0 decimals TokenStandard::NonFungible | TokenStandard::ProgrammableNonFungible => 0, // for Fungile variants, we either use the specified decimals or the default // DECIMALS from spl-token TokenStandard::FungibleAsset | TokenStandard::Fungible => match decimals { Some(decimals) => decimals, // if decimals not provided, use the default None => DECIMALS, }, _ => { return Err(MetadataError::InvalidTokenStandard.into()); } }; // initializing the mint account invoke( &spl_token_2022::instruction::initialize_mint2( spl_token_program.key, mint.key, authority.key, Some(authority.key), decimals, )?, &[mint.clone(), authority.clone()], ) }

Metadata Account Creation

process_create_metadata_accounts_logic handles creating metadata account according to the parameter and a set of rules, and initialize metadata.
Detailed analysis can be found here.
/// --- programs/token-metadata/program/src/utils/metadata.rs --- #[allow(clippy::too_many_arguments)] /// Create a new account instruction #[allow(clippy::too_many_arguments)] pub fn process_create_metadata_accounts_logic( program_id: &Pubkey, accounts: CreateMetadataAccountsLogicArgs, data: DataV2, allow_direct_creator_writes: bool, mut is_mutable: bool, is_edition: bool, add_token_standard: bool, collection_details: Option<CollectionDetails>, token_standard_override: Option<TokenStandard>, programmable_config: Option<ProgrammableConfig>, ) -> ProgramResult { let CreateMetadataAccountsLogicArgs { metadata_account_info, mint_info, mint_authority_info, payer_account_info, update_authority_info, system_account_info, } = accounts; let mut update_authority_key = *update_authority_info.key; let existing_mint_authority = get_mint_authority(mint_info)?; // IMPORTANT NOTE: // This allows the Metaplex Foundation to Create but not update metadata for SPL tokens that // have not populated their metadata. assert_mint_authority_matches_mint(&existing_mint_authority, mint_authority_info).or_else( |e| { // Allow seeding by the authority seed populator if mint_authority_info.key == &SEED_AUTHORITY && mint_authority_info.is_signer { // When metadata is seeded, the mint authority should be able to change it if let COption::Some(auth) = existing_mint_authority { update_authority_key = auth; is_mutable = true; } Ok(()) } else { Err(e) } }, )?; assert_owner_in(mint_info, &SPL_TOKEN_PROGRAM_IDS)?; let metadata_seeds = &[ PREFIX.as_bytes(), program_id.as_ref(), mint_info.key.as_ref(), ]; let (metadata_key, metadata_bump_seed) = Pubkey::find_program_address(metadata_seeds, program_id); let metadata_authority_signer_seeds = &[ PREFIX.as_bytes(), program_id.as_ref(), mint_info.key.as_ref(), &[metadata_bump_seed], ]; if metadata_account_info.key != &metadata_key { return Err(MetadataError::InvalidMetadataKey.into()); } create_or_allocate_account_raw( *program_id, metadata_account_info, system_account_info, payer_account_info, MAX_METADATA_LEN, metadata_authority_signer_seeds, )?; let mut metadata = Metadata::from_account_info(metadata_account_info)?; let compatible_data = data.to_v1(); // This allows the Bubblegum program to create metadata with verified creators since they were // verified already by the Bubblegum program. let is_decompression = is_decompression(mint_info, mint_authority_info); let allow_direct_creator_writes = allow_direct_creator_writes || is_decompression; assert_data_valid( &compatible_data, &update_authority_key, &metadata, allow_direct_creator_writes, update_authority_info.is_signer, )?; let mint_decimals = get_mint_decimals(mint_info)?; metadata.mint = *mint_info.key; metadata.key = Key::MetadataV1; metadata.data = data.to_v1(); metadata.is_mutable = is_mutable; metadata.update_authority = update_authority_key; metadata.programmable_config = programmable_config; assert_valid_use(&data.uses, &None)?; metadata.uses = data.uses; // This allows for either print editions or the Bubblegum program to create metadata with verified collection. let allow_direct_collection_verified_writes = is_edition || is_decompression; assert_collection_update_is_valid( allow_direct_collection_verified_writes, &None, &data.collection, )?; metadata.collection = data.collection; // We want to create new collections with a size of zero but we use the // collection details enum for forward compatibility. if let Some(details) = collection_details { match details { #[allow(deprecated)] CollectionDetails::V1 { size: _size } => { metadata.collection_details = Some(CollectionDetails::V1 { size: 0 }); } CollectionDetails::V2 { padding: _ } => { metadata.collection_details = None; } } } else { metadata.collection_details = None; } metadata.token_standard = if add_token_standard { token_standard_override.or({ if is_edition { Some(TokenStandard::NonFungibleEdition) } else if mint_decimals == 0 { Some(TokenStandard::FungibleAsset) } else { Some(TokenStandard::Fungible) } }) } else { None }; puff_out_data_fields(&mut metadata); let edition_seeds = &[ PREFIX.as_bytes(), program_id.as_ref(), metadata.mint.as_ref(), EDITION.as_bytes(), ]; let (_, edition_bump_seed) = Pubkey::find_program_address(edition_seeds, program_id); metadata.edition_nonce = Some(edition_bump_seed); // saves the changes to the account data metadata.save(&mut metadata_account_info.data.borrow_mut())?; Ok(()) }

Master Edition Account Creation

For non-fungible token, the supply should always be 1. So the mint_authority should be restrictly controlled. We can set it to be None, but in that case we may lose flexibility to do more operations in the future, such as NFT migration where users need to burn their NFT and the system can mint another new NFT to them. In that case, we need reserve the mint functionality. The same logic applies to freeze_authority of the Mint account.
So metaplex protocol chooses to reserve the mint_authority and freeze_authority. Rather than set them to be None, it assigns them to a PDA which is called Edition with metaplex program as the owner. So to perform the authority, we can access metaplex program to get the signer role and performs corresponding operations on Mint/ATA account. Currently, metaplex program has no instruction to perform such operations.
 
In the process, it creates a master edition account for non-fungible assets (NFTs and pNFTs) and store relevant data, such as print supply and token standard, within the master edition account.
Steps:
  1. Require print_supply is provided.
  1. Create master edition account.
  1. If the mint is pNFT, then add TokenStandard to the data in master edition account.
 
Note that if we don’t provide master_edition , we can create NFT without master edition account. In this case, the authority of the mint will be the authority account we pass in(refer)
/// --- programs/token-metadata/program/src/processor/metadata/create.rs --- // creates the master edition account (only for NonFungible assets) if matches!( asset_data.token_standard, TokenStandard::NonFungible | TokenStandard::ProgrammableNonFungible ) { /// Require print_supply is provided. let print_supply = print_supply.ok_or(MetadataError::MissingPrintSupply)?; if let Some(master_edition) = ctx.accounts.master_edition_info { let spl_token_program = ctx .accounts .spl_token_program_info .ok_or(MetadataError::MissingSplTokenProgram)?; /// Create master edition account. create_master_edition( program_id, master_edition, ctx.accounts.mint_info, ctx.accounts.update_authority_info, ctx.accounts.authority_info, ctx.accounts.payer_info, ctx.accounts.metadata_info, spl_token_program, ctx.accounts.system_program_info, print_supply.to_option(), )?; // for pNFTs, we store the token standard value at the end of the // master edition account if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible ) { let mut data = master_edition.data.borrow_mut(); if data.len() < MAX_MASTER_EDITION_LEN { return Err(MetadataError::InvalidMasterEditionAccountLength.into()); } data[TOKEN_STANDARD_INDEX] = TokenStandard::ProgrammableNonFungible as u8; } } else { return Err(MetadataError::MissingMasterEditionAccount.into()); } } else if print_supply.is_some() { msg!("Ignoring print supply for selected token standard"); } /// --- programs/token-metadata/program/src/state/master_edition.rs --- // Large buffer because the older master editions have two pubkeys in them, // need to keep two versions same size because the conversion process actually // changes the same account by rewriting it. pub const MAX_MASTER_EDITION_LEN: usize = 1 + 9 + 8 + 264; // The last byte of the account containts the token standard value for // pNFT assets. This is used to restrict legacy operations on the master // edition account. pub const TOKEN_STANDARD_INDEX: usize = MAX_MASTER_EDITION_LEN - 1;
 

Create master edition account

create_master_edition function's primary purpose is to create a master edition account for non-fungible assets, ensuring that the mint account adheres to the constraints of an NFT (e.g., mint supply of either 0 or 1).
 
MasterEditionV2 type master account will be created.
Data:
  1. key
    1. type of the edition account
  1. supply
    1. current supply of the print NFT.
  1. max_supply
    1. max supply of print NFT.
      • None: The NFT does not have a fixed supply. In other words, the NFT is printable and has an unlimited supply.
      • Some(x): The NFT has a fixed supply of x editions.
        • When x = 0, this means the NFT is not printable.
        • When x > 0, this means the NFT is printable and has a fixed supply of x editions.
      In our instruction args, we can see it is set to be 0, which means it doesn’t allow print NFT.
/// --- programs/token-metadata/program/src/state/master_edition.rs --- #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(Clone, Debug, PartialEq, Eq, BorshSerialize, BorshDeserialize, ShankAccount)] pub struct MasterEditionV2 { pub key: Key, pub supply: u64, pub max_supply: Option<u64>, } /// --- programs/token-metadata/program/src/state/mod.rs --- #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy, FromPrimitive)] pub enum Key { Uninitialized, EditionV1, MasterEditionV1, ReservationListV1, MetadataV1, ReservationListV2, MasterEditionV2, EditionMarker, UseAuthorityRecord, CollectionAuthorityRecord, TokenOwnedEscrow, TokenRecord, MetadataDelegate, EditionMarkerV2, HolderDelegate, }
 
Steps:
  1. Deserialize Metadata
    1. Deserialize Metadata Stored In Metadata Account. Also check that the Metadata account belongs to metaplex program.
  1. Deserialize MINT data
    1. Deserialize MINT data stored in MINT account.
  1. Check Master Edition Account Is Derived From Metaplex Program
    1. So that Metaplex Program can successfully create and manage the Master Edition Account.
  1. Check Validity of Passed-in SPL Token Program
    1. Check it is one of the official SPL token programs.
  1. Check passed-in Mint Authority
    1. Check it is the owner of the MINT account, and it is signer.
  1. Check Metadata Account is Owned by Metaplex program
  1. Check Mint Account is owned by SPL Token Program
  1. Check Metadata Account’s Mint matches Mint Account
  1. Check Mint Account’s Decimal is Zero
  1. Check Metadata Update Authority’s Validity
      • Metadata account’s update authority is the passed-in update authority account
      • Passed-in update authority account is signer.
  1. Check Mint Account Supply not Larger Than 1
  1. Create Master Edition Account
    1. allocate space and assign owner to metaplex program.
  1. Update Master Edition Data
  1. Update Metadata Account Data
    1. Set token standard to TokenStandard::NonFungible
  1. Transfer Mint and Freezer Authority to the Master Edition Account
/// --- programs/token-metadata/program/src/utils/master_edition.rs --- /// Creates a new master edition account for the specified `edition_account_info` and /// `mint_info`. Master editions only exist for non-fungible assets, therefore the supply /// of the mint must thei either 0 or 1; any value higher than that will generate an /// error. /// /// After a master edition is created, it becomes the mint authority of the mint account. #[allow(clippy::too_many_arguments)] pub fn create_master_edition<'a>( program_id: &Pubkey, edition_account_info: &'a AccountInfo<'a>, mint_info: &'a AccountInfo<'a>, update_authority_info: &'a AccountInfo<'a>, mint_authority_info: &'a AccountInfo<'a>, payer_account_info: &'a AccountInfo<'a>, metadata_account_info: &'a AccountInfo<'a>, token_program_info: &'a AccountInfo<'a>, system_account_info: &'a AccountInfo<'a>, max_supply: Option<u64>, ) -> ProgramResult { /// Deserialize Metadata let mut metadata = Metadata::from_account_info(metadata_account_info)?; /// Deserialize MINT data let mint = unpack_initialized::<Mint>(&mint_info.data.borrow())?; /// Check Master Edition Account Is Derived From Metaplex Program let bump_seed = assert_derivation( program_id, edition_account_info, &[ PREFIX.as_bytes(), program_id.as_ref(), mint_info.key.as_ref(), EDITION.as_bytes(), ], )?; /// Check Validity of Passed-in SPL Token Program assert_token_program_matches_package(token_program_info)?; /// Check passed-in Mint Authority assert_mint_authority_matches_mint(&mint.mint_authority, mint_authority_info)?; /// Check Metadata Account is Owned by Metaplex program assert_owned_by(metadata_account_info, program_id)?; /// Check Mint Account is owned by SPL Token Program assert_owned_by(mint_info, token_program_info.key)?; /// Check Metadata Account’s Mint matches Mint Account if metadata.mint != *mint_info.key { return Err(MetadataError::MintMismatch.into()); } /// Check Mint Account’s Decimal is Zero if mint.decimals != 0 { return Err(MetadataError::EditionMintDecimalsShouldBeZero.into()); } /// Check Metadata Update Authority’s Validity assert_update_authority_is_correct(&metadata, update_authority_info)?; /// Check Mint Account Supply not Larger Than 1 if mint.supply > 1 { return Err(MetadataError::EditionsMustHaveExactlyOneToken.into()); } /// Create Master Edition Account let edition_authority_seeds = &[ PREFIX.as_bytes(), program_id.as_ref(), mint_info.key.as_ref(), EDITION.as_bytes(), &[bump_seed], ]; create_or_allocate_account_raw( *program_id, edition_account_info, system_account_info, payer_account_info, MAX_MASTER_EDITION_LEN, edition_authority_seeds, )?; /// Update Master Edition Data let mut edition = MasterEditionV2::from_account_info(edition_account_info)?; edition.key = Key::MasterEditionV2; edition.supply = 0; edition.max_supply = max_supply; edition.save(edition_account_info)?; /// Update Metadata Account Data if metadata_account_info.is_writable { metadata.token_standard = Some(TokenStandard::NonFungible); metadata.save(&mut metadata_account_info.try_borrow_mut_data()?)?; } /// Transfer Mint and Freezer Authority to the Master Edition Account // while you can mint only 1 token from your master record, you can // mint as many limited editions as you like within your max supply transfer_mint_authority( edition_account_info.key, edition_account_info, mint_info, mint_authority_info, token_program_info, ) }
 
Allocate space and assign owner of Master Edition Account
/// --- index.crates.io-6f17d22bba15001f/mpl-utils-0.3.4/src/account.rs --- /// Create account almost from scratch, lifted from /// <https://github.com/solana-labs/solana-program-library/tree/master/associated-token-account/program/src/processor.rs#L51-L98> pub fn create_or_allocate_account_raw<'a>( program_id: Pubkey, new_account_info: &AccountInfo<'a>, system_program_info: &AccountInfo<'a>, payer_info: &AccountInfo<'a>, size: usize, signer_seeds: &[&[u8]], ) -> ProgramResult { let rent = &Rent::get()?; let required_lamports = rent .minimum_balance(size) .max(1) .saturating_sub(new_account_info.lamports()); if required_lamports > 0 { msg!("Transfer {} lamports to the new account", required_lamports); invoke( &system_instruction::transfer(payer_info.key, new_account_info.key, required_lamports), &[ payer_info.clone(), new_account_info.clone(), system_program_info.clone(), ], )?; } let accounts = &[new_account_info.clone(), system_program_info.clone()]; msg!("Allocate space for the account"); invoke_signed( &system_instruction::allocate(new_account_info.key, size.try_into().unwrap()), accounts, &[signer_seeds], )?; msg!("Assign the account to the owning program"); invoke_signed( &system_instruction::assign(new_account_info.key, &program_id), accounts, &[signer_seeds], )?; Ok(()) }
 
Transfer mint account mint authority and freeze authority to master edition
/// ---programs/token-metadata/program/src/utils/mod.rs--- pub fn transfer_mint_authority<'a>( edition_key: &Pubkey, edition_account_info: &AccountInfo<'a>, mint_info: &AccountInfo<'a>, mint_authority_info: &AccountInfo<'a>, token_program_info: &AccountInfo<'a>, ) -> ProgramResult { let accounts = &[ mint_authority_info.clone(), mint_info.clone(), token_program_info.clone(), edition_account_info.clone(), ]; invoke_signed( &set_authority( token_program_info.key, mint_info.key, Some(edition_key), AuthorityType::MintTokens, mint_authority_info.key, &[mint_authority_info.key], ) .unwrap(), accounts, &[], )?; let freeze_authority = get_mint_freeze_authority(mint_info)?; if freeze_authority.is_some() { invoke_signed( &set_authority( token_program_info.key, mint_info.key, Some(edition_key), AuthorityType::FreezeAccount, mint_authority_info.key, &[mint_authority_info.key], ) .unwrap(), accounts, &[], )?; } else { return Err(MetadataError::NoFreezeAuthoritySet.into()); } Ok(()) }

Metadata State Update

Update token_standard, primary_sale_happened and programmable_config in the metadata account.
/// --- programs/token-metadata/program/src/processor/metadata/create.rs --- /// V1 implementation of the create instruction. fn create_v1(program_id: &Pubkey, ctx: Context<Create>, args: CreateArgs) -> ProgramResult { /// ... let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; metadata.token_standard = Some(asset_data.token_standard); metadata.primary_sale_happened = asset_data.primary_sale_happened; // sets the programmable config for programmable assets if matches!( asset_data.token_standard, TokenStandard::ProgrammableNonFungible ) { metadata.programmable_config = Some(ProgrammableConfig::V1 { rule_set: asset_data.rule_set, }); } // saves the metadata state metadata.save(&mut ctx.accounts.metadata_info.try_borrow_mut_data()?)?; /// ... }

Set Fee Flag

set_fee_flag sets the last byte of metadata account to be 1 to indicate that the metadata account has fee remained to be withdrawed by metaplex protocol team.
/// --- programs/token-metadata/program/src/utils/fee.rs --- pub(crate) fn set_fee_flag(pda_account_info: &AccountInfo) -> ProgramResult { let mut data = pda_account_info.try_borrow_mut_data()?; data[METADATA_FEE_FLAG_INDEX] = 1; Ok(()) } /// --- programs/token-metadata/program/src/state/metadata.rs --- pub const MAX_DATA_SIZE: usize = 4 + MAX_NAME_LENGTH + 4 + MAX_SYMBOL_LENGTH + 4 + MAX_URI_LENGTH + 2 + 1 + 4 + MAX_CREATOR_LIMIT * MAX_CREATOR_LEN; // The last byte of the account contains the fee flag, indicating // if the account has fees available for retrieval. pub const METADATA_FEE_FLAG_INDEX: usize = MAX_METADATA_LEN - 1;

Creat Collection # Mint Collection NFT

Metaplex uses MetadataInstruction:Mint to mint collection token to collection creator. For non fungible token, the mint authority is set already to be master edition. So to mint non fungible token, it requires authority of update_authority of token’s metadata account.

Instruction

/// --- programs/token-metadata/program/src/instruction/mod.rs --- pub enum MetadataInstruction { /// ... /// Mints tokens from a mint account into the specified token account. /// /// This instruction will also initialized the associated token account if it does not exist – in /// this case the `token_owner` will be required. When minting `*NonFungible` assets, the `authority` /// must be the update authority; in all other cases, it must be the mint authority from the mint /// account. #[account(0, writable, name="token", desc="Token or Associated Token account")] #[account(1, optional, name="token_owner", desc="Owner of the token account")] #[account(2, name="metadata", desc="Metadata account (pda of ['metadata', program id, mint id])")] #[account(3, optional, writable, name="master_edition", desc="Master Edition account")] #[account(4, optional, writable, name="token_record", desc="Token record account")] #[account(5, writable, name="mint", desc="Mint of token asset")] #[account(6, signer, name="authority", desc="(Mint or Update) authority")] #[account(7, optional, name="delegate_record", desc="Metadata delegate record")] #[account(8, signer, writable, name="payer", desc="Payer")] #[account(9, name="system_program", desc="System program")] #[account(10, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(11, name="spl_token_program", desc="SPL Token program")] #[account(12, name="spl_ata_program", desc="SPL Associated Token Account program")] #[account(13, optional, name="authorization_rules_program", desc="Token Authorization Rules program")] #[account(14, optional, name="authorization_rules", desc="Token Authorization Rules account")] Mint(MintArgs), /// ... }
 

Execute Instruction

mint iterates accounts to extract accounts and store tham in context. Then it calls mint_v1 to mint collection token.
/// --- programs/token-metadata/program/src/processor/metadata/mint.rs --- /// Mints tokens from a mint account. /// /// This instruction will also initialized the associated token account if it does not exist – in /// this case the `token_owner` will be required. When minting `*NonFungible` assets, the `authority` /// must be the update authority; in all other cases, it must be the mint authority from the mint /// account. pub fn mint<'a>( program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], args: MintArgs, ) -> ProgramResult { let context = Mint::to_context(accounts)?; match args { MintArgs::V1 { .. } => mint_v1(program_id, context, args), } } /// --- programs/token-metadata/program/src/instruction/metadata.rs ---- pub enum MintArgs { V1 { amount: u64, /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, }
 
The mint_v1 function is responsible for minting new tokens, including both fungible and non-fungible tokens (NFTs).
Steps:
  1. Argument Destructuring and Validation
      • The amount to be minted is extracted from the args.
      • The function immediately returns an error if amount is 0, as minting zero tokens is not allowed.
  1. Signer Validation
      • checks that both the authority_info and payer_info accounts are signers. This ensures that the minting operation is authorized and that the payer is providing the necessary resources.
  1. Account Ownership and Derivation Validation
      • The function verifies that the metadata_info account is owned by the correct program (program_id).
      • It also checks that the metadata_info account is derived correctly based on the mint public key and the program ID, ensuring the metadata is generated through Metaplex protocol.
  1. Metadata and Mint Matching
    1. loads the metadata from the metadata_info account and ensures that the mint account in the metadata matches the mint account provided in the context. If they do not match, it returns a MintMismatch error.
  1. Token Program Validation
      • The function verifies that the token program used is the expected SPL Token program.
      • It also checks that the mint_info account is owned by this token program.
  1. Unpack Mint data
    1. The mint_info account is unpacked to retrieve the mint details.
  1. Token Standard Handling
      • Non-Fungible Tokens (NFTs):
        • It requires the presence of a master edition account. Because for NFT, the mint authority is transferred eto master edition account.
        • Checks that the update_authority is correct.
        • Ensures that only one token is minted (amount must be 1).
      • Fungible Tokens:
        • Ensures the mint authority matches the provided authority.
  1. Token Account Initialization
    1. If the token account does not exist (data_is_empty), the function creates a new associated token account using the SPL Associated Token Account program.
  1. Token Record Handling (for Programmable NFTs)
    1. For Programmable NFTs (pNFTs), the function ensures the existence and validity of the token record account.
  1. Thawing and Freezing Accounts
      • For pNFTs, if the token account is frozen, it is thawed before minting.
      • After minting, pNFTs are refrozen to ensure they remain in a secure state.
  1. Final Minting
/// --- programs/token-metadata/program/src/processor/metadata/mint.rs --- pub fn mint_v1(program_id: &Pubkey, ctx: Context<Mint>, args: MintArgs) -> ProgramResult { /// Argument Destructuring and Validation // get the args for the instruction let MintArgs::V1 { amount, .. } = args; if amount == 0 { return Err(MetadataError::AmountMustBeGreaterThanZero.into()); } /// Signer Validation // checks that we have the required signers assert_signer(ctx.accounts.authority_info)?; assert_signer(ctx.accounts.payer_info)?; /// Account Ownership and Derivation Validation // validates the accounts assert_owned_by(ctx.accounts.metadata_info, program_id)?; assert_derivation( program_id, ctx.accounts.metadata_info, &[ PREFIX.as_bytes(), program_id.as_ref(), ctx.accounts.mint_info.key.as_ref(), ], )?; /// Metadata and Mint Matching let metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; if metadata.mint != *ctx.accounts.mint_info.key { return Err(MetadataError::MintMismatch.into()); } /// Token Program Validation assert_token_program_matches_package(ctx.accounts.spl_token_program_info)?; assert_owned_by( ctx.accounts.mint_info, ctx.accounts.spl_token_program_info.key, )?; /// Unpack Mint data let mint = unpack_initialized::<MintAccount>(&ctx.accounts.mint_info.data.borrow())?; /// Token Standard Handling // validates the authority: // - NonFungible must have a "valid" master edition // - Fungible must have the authority as the mint_authority match metadata.token_standard { Some(TokenStandard::ProgrammableNonFungible) | Some(TokenStandard::NonFungible) => { // for NonFungible assets, the mint authority is the master edition if let Some(master_edition_info) = ctx.accounts.master_edition_info { assert_derivation( program_id, master_edition_info, &[ PREFIX.as_bytes(), program_id.as_ref(), ctx.accounts.mint_info.key.as_ref(), EDITION.as_bytes(), ], )?; } else { return Err(MetadataError::MissingMasterEditionAccount.into()); } if mint.supply > 0 || amount > 1 { return Err(MetadataError::EditionsMustHaveExactlyOneToken.into()); } // authority must be the update_authority of the metadata account if !cmp_pubkeys(&metadata.update_authority, ctx.accounts.authority_info.key) { return Err(MetadataError::UpdateAuthorityIncorrect.into()); } } _ => { assert_mint_authority_matches_mint(&mint.mint_authority, ctx.accounts.authority_info)?; } } /// Token Account Initialization // validates the token account if ctx.accounts.token_info.data_is_empty() { // if we are initializing a new account, we need the token_owner let token_owner_info = if let Some(token_owner_info) = ctx.accounts.token_owner_info { token_owner_info } else { return Err(MetadataError::MissingTokenOwnerAccount.into()); }; // creating the associated token account invoke( &spl_associated_token_account::instruction::create_associated_token_account( ctx.accounts.payer_info.key, token_owner_info.key, ctx.accounts.mint_info.key, ctx.accounts.spl_token_program_info.key, ), &[ ctx.accounts.payer_info.clone(), token_owner_info.clone(), ctx.accounts.mint_info.clone(), ctx.accounts.token_info.clone(), ], )?; } else { let token = validate_token( ctx.accounts.mint_info, ctx.accounts.token_info, ctx.accounts.token_owner_info, ctx.accounts.spl_token_program_info, metadata.token_standard, None, // we already checked the supply of the mint account )?; // validates that the close authority on the token is either None // or the master edition account for programmable assets if matches!( metadata.token_standard, Some(TokenStandard::ProgrammableNonFungible) | Some(TokenStandard::ProgrammableNonFungibleEdition) ) { if let COption::Some(close_authority) = token.close_authority { // the close authority must match the master edition if there is one set // on the token account if let Some(master_edition) = ctx.accounts.master_edition_info { if close_authority != *master_edition.key { return Err(MetadataError::InvalidCloseAuthority.into()); } } else { return Err(MetadataError::MissingMasterEditionAccount.into()); }; } } } let token = unpack_initialized::<Account>(&ctx.accounts.token_info.data.borrow())?; match metadata.token_standard { Some(TokenStandard::NonFungible) | Some(TokenStandard::ProgrammableNonFungible) => { /// Token Record Handling (for Programmable NFTs) // for pNFTs, we require the token record account if matches!( metadata.token_standard, Some(TokenStandard::ProgrammableNonFungible) ) { // we always need the token_record_info let token_record_info = ctx .accounts .token_record_info .ok_or(MetadataError::MissingTokenRecord)?; let (pda_key, _) = find_token_record_account( ctx.accounts.mint_info.key, ctx.accounts.token_info.key, ); // validates the derivation assert_keys_equal(&pda_key, token_record_info.key)?; if token_record_info.data_is_empty() { create_token_record_account( program_id, token_record_info, ctx.accounts.mint_info, ctx.accounts.token_info, ctx.accounts.payer_info, ctx.accounts.system_program_info, )?; } else { assert_owned_by(token_record_info, &crate::ID)?; } } let master_edition_seeds = &[ PREFIX.as_bytes(), program_id.as_ref(), ctx.accounts.mint_info.key.as_ref(), EDITION.as_bytes(), &[metadata .edition_nonce .ok_or(MetadataError::NotAMasterEdition)?], ]; let master_edition_info = ctx .accounts .master_edition_info .ok_or(MetadataError::MissingMasterEditionAccount)?; assert_derivation_with_bump( &crate::ID, master_edition_info, master_edition_seeds, MetadataError::InvalidMasterEdition, )?; /// Thawing and Freezing Accounts // thaw the token account for programmable assets; the account // is not frozen if we just initialized it if matches!( metadata.token_standard, Some(TokenStandard::ProgrammableNonFungible) ) && token.is_frozen() { thaw( ctx.accounts.mint_info.clone(), ctx.accounts.token_info.clone(), master_edition_info.clone(), ctx.accounts.spl_token_program_info.clone(), metadata.edition_nonce, )?; } /// Final Minting (NFT) invoke_signed( &spl_token_2022::instruction::mint_to( ctx.accounts.spl_token_program_info.key, ctx.accounts.mint_info.key, ctx.accounts.token_info.key, master_edition_info.key, &[], amount, )?, &[ ctx.accounts.mint_info.clone(), ctx.accounts.token_info.clone(), master_edition_info.clone(), ], &[master_edition_seeds], )?; // programmable assets are always in a frozen state if matches!( metadata.token_standard, Some(TokenStandard::ProgrammableNonFungible) ) { freeze( ctx.accounts.mint_info.clone(), ctx.accounts.token_info.clone(), master_edition_info.clone(), ctx.accounts.spl_token_program_info.clone(), metadata.edition_nonce, )?; } } /// Final Minting (FT) _ => { invoke( &spl_token_2022::instruction::mint_to( ctx.accounts.spl_token_program_info.key, ctx.accounts.mint_info.key, ctx.accounts.token_info.key, ctx.accounts.authority_info.key, &[], amount, )?, &[ ctx.accounts.mint_info.clone(), ctx.accounts.token_info.clone(), ctx.accounts.authority_info.clone(), ], )?; } } Ok(()) }

Create NFT # Create Mint

Instruction

Metaplex uses same Create instruction to create NFT with difference in the createArgs.
/// --- program/src/instruction/mod.rs --- #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] /// Instructions supported by the Metadata program. #[derive(BorshSerialize, BorshDeserialize, Clone, ShankInstruction, AccountContext)] #[rustfmt::skip] pub enum MetadataInstruction { /// ... /// Creates the metadata and associated accounts for a new or existing mint account. /// /// This instruction will initialize a mint account if it does not exist and /// the mint key is a signer on the transaction. /// /// When creating a non-fungible assert, the `master_edition` needs to be specified. #[account(0, writable, name="metadata", desc="Unallocated metadata account with address as pda of ['metadata', program id, mint id]")] #[account(1, optional, writable, name="master_edition", desc="Unallocated edition account with address as pda of ['metadata', program id, mint, 'edition']")] #[account(2, writable, name="mint", desc="Mint of token asset")] #[account(3, signer, name="authority", desc="Mint authority")] #[account(4, signer, writable, name="payer", desc="Payer")] #[account(5, name="update_authority", desc="Update authority for the metadata account")] #[account(6, name="system_program", desc="System program")] #[account(7, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(8, optional, name="spl_token_program", desc="SPL Token program")] #[args(initialize_mint: bool)] #[args(update_authority_as_signer: bool)] Create(CreateArgs), /// ... }
 
In the nft creation tx, we can see the createArgs.
Compared to createArgs in collection creation:
  1. it has argumenet collection which refers the collection token address, collection helps group collection NFTs together.
  1. it doesn’t have collectionDetails argument because thisi is a NFT and collectionDetails is used to distinguish collection from NFT.
 
So the whole NFT creation process is same with collection create process, with difference that the created NFT’s metadata account has collection field but no collectionDetails.
Note that the collection.verified field is used to verify whether a collection NFT indeed belongs to the collection, without the check, anyone can create NFT and pretend it to belong to any collection. And this field and only be flipped by authority of the collection.
{ "assetData": { "name": "Hello Solana NFT", "symbol": "", "uri": "www.nftUri.com", "sellerFeeBasisPoints": 0, "creators": [ { "address": "2FdSZMUKM8ZighMnurwNuboAFm8wAdqrERwimxtrXdR6", "verified": true, "share": 100 } ], "primarySaleHappened": false, "isMutable": true, "tokenStandard": { "enumType": "nonFungible" }, "collection": { "verified": false, "key": "39obDjVzcxcK6fDn4BuBYa2Y1rC8gJnedVJZVVD1DGzC" }, "uses": null, "collectionDetails": null, "ruleSet": null }, "decimals": 0, "printSupply": { "enumType": "zero" }, "enumType": "v1" }
 

Create NFT # Mint NFT

Instruction

Metaplex uses MetadataInstruction:Mint to mint NFT to collection.

Verify

Instruction

Collection authority can use Verify instruction to set the collection.verified field of NFT to be true.
pub enum MetadataInstruction { /// ... /// Verifies that an asset was created by a specific creator or belongs in an specified collection. /// /// Depending on the type of verification (e.g., creator or collection), additional accounts /// are required. #[account(0, signer, name="authority", desc="Creator to verify, collection update authority or delegate")] #[account(1, optional, name="delegate_record", desc="Delegate record PDA")] #[account(2, writable, name="metadata", desc="Metadata account")] #[account(3, optional, name="collection_mint", desc="Mint of the Collection")] #[account(4, optional, writable, name="collection_metadata", desc="Metadata Account of the Collection")] #[account(5, optional, name="collection_master_edition", desc="Master Edition Account of the Collection Token")] #[account(6, name="system_program", desc="System program")] #[account(7, name="sysvar_instructions", desc="Instructions sysvar account")] Verify(VerificationArgs), /// ... }

Execute

In the verify function, we can see there are two verification method. One is to verify token’s creator, the other is to verify collection.
/// --- programs/token-metadata/program/src/processor/verification/verify.rs --- pub fn verify<'a>( program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], args: VerificationArgs, ) -> ProgramResult { let context = Verify::to_context(accounts)?; match args { VerificationArgs::CreatorV1 => verify_creator_v1(program_id, context), VerificationArgs::CollectionV1 => verify_collection_v1(program_id, context), } }
 

verify_creator_v1

verify_creator_v1 checks the creator is a signer and is included in the creator set of metadata account. Then it flips the verified field to be true, and save the metadata.
/// --- programs/token-metadata/program/src/processor/verification/creator.rs --- pub(crate) fn verify_creator_v1(program_id: &Pubkey, ctx: Context<Verify>) -> ProgramResult { // Assert program ownership/signers. // Authority account is the creator and must be a signer. assert_signer(ctx.accounts.authority_info)?; assert_owned_by(ctx.accounts.metadata_info, program_id)?; // Deserialize item metadata. let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; // Find creator in creator array and if found mark as verified. find_and_set_creator( &mut metadata.data.creators, *ctx.accounts.authority_info.key, true, )?; // Reserialize item metadata. clean_write_metadata(&mut metadata, ctx.accounts.metadata_info) } /// --- programs/token-metadata/program/src/processor/verification/creator.rs --- fn find_and_set_creator( creators: &mut Option<Vec<Creator>>, creator_to_match: Pubkey, verified: bool, ) -> ProgramResult { // Find creator in creator array and if found mark as verified. match creators { Some(creators) => { let creator = creators .iter_mut() .find(|c| c.address == creator_to_match) .ok_or(MetadataError::CreatorNotFound)?; creator.verified = verified; Ok(()) } None => Err(MetadataError::NoCreatorsPresentOnMetadata.into()), } }
 
verify_collection_v1 is responsible for verifying that a particular NFT belongs to a specified collection. This function is part of the token metadata program and involves validating the relationship between the NFT and its collection, ensuring the integrity of the metadata, and updating the collection verification status.
 
Steps:
  1. Signers and Ownership Checks:
      • The function starts by verifying that the authority account is a signer using assert_signer(ctx.accounts.authority_info)?.
      • It checks that the metadata_info account (the account holding the NFT's metadata) is owned by the program using assert_owned_by.
      • Similar ownership checks are performed on collection_mint_info, collection_metadata_info, and collection_master_edition_info.
  1. Loading Metadata:
      • The function then deserializes the metadata from metadata_info and collection_metadata_info accounts.
      • It checks if the collection has already been verified to avoid redundant operations.
  1. Collection Validation:
    1. The function validates that the collection field in the NFT's metadata matches the actual collection. It ensures that the collection_mint_info and collection_master_edition_info are valid by calling assert_collection_verify_is_valid.
  1. Authority Validation:
      • The function determines if the authority provided (in authority_info) is allowed to verify the collection. This is done using AuthorityType::get_authority_type, which checks if the authority is either the metadata update authority or a collection delegate.
      • If the authority is valid, the function proceeds; otherwise, it returns an error.
  1. Collection Verification:
      • If the NFT's collection field is present and not already verified, the function sets the verified field to true.
      • If the collection is a "sized collection" (i.e., a collection with a size limit), the function increments the collection size using increment_collection_size.
  1. Updating Metadata:
    1. Finally, the function writes the updated metadata back to the metadata_info account using clean_write_metadata.
/// --- programs/token-metadata/program/src/processor/verification/collection.rs --- pub(crate) fn verify_collection_v1(program_id: &Pubkey, ctx: Context<Verify>) -> ProgramResult { /// Signers and Ownership Checks: // Assert program ownership/signers. // Authority account must be a signer. What this authority account actually represents is // checked below. assert_signer(ctx.accounts.authority_info)?; // Note: `ctx.accounts.delegate_record_info` owner check done inside of `get_authority_type`. assert_owned_by(ctx.accounts.metadata_info, program_id)?; let collection_mint_info = ctx .accounts .collection_mint_info .ok_or(MetadataError::MissingCollectionMint)?; assert_owner_in(collection_mint_info, &SPL_TOKEN_PROGRAM_IDS)?; let collection_metadata_info = ctx .accounts .collection_metadata_info .ok_or(MetadataError::MissingCollectionMetadata)?; assert_owned_by(collection_metadata_info, program_id)?; let collection_master_edition_info = ctx .accounts .collection_master_edition_info .ok_or(MetadataError::MissingCollectionMasterEdition)?; assert_owned_by(collection_master_edition_info, program_id)?; /// Loading Metadata: // Deserialize item metadata and collection parent metadata. let mut metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; let mut collection_metadata = Metadata::from_account_info(collection_metadata_info)?; /// // Short circuit if its already verified. If we let the rest of this instruction run, then for // sized collections we would end up with invalid size data. if let Some(collection) = &metadata.collection { if collection.verified { return Ok(()); } } /// Collection Validation: // Verify the collection in the item's metadata matches the collection mint. Also verify // the collection metadata matches the collection mint, and the collection edition derivation. assert_collection_verify_is_valid( &metadata.collection, &collection_metadata, collection_mint_info, collection_master_edition_info, )?; /// Authority Validation: // Determines if we have a valid authority to perform the collection verification. The // required authority is either the collection parent's metadata update authority, or a // collection delegate for the collection parent. This call fails if no valid authority is // present. let authority_response = AuthorityType::get_authority_type(AuthorityRequest { authority: ctx.accounts.authority_info.key, update_authority: &collection_metadata.update_authority, mint: collection_mint_info.key, metadata_delegate_record_info: ctx.accounts.delegate_record_info, metadata_delegate_roles: vec![MetadataDelegateRole::Collection], precedence: &[AuthorityType::Metadata, AuthorityType::MetadataDelegate], ..Default::default() })?; // Validate that authority type is expected. match authority_response.authority_type { AuthorityType::Metadata | AuthorityType::MetadataDelegate => (), _ => return Err(MetadataError::UpdateAuthorityIncorrect.into()), } /// Collection Verification: // Destructure the collection field from the item metadata. match metadata.collection.as_mut() { Some(collection) => { // Set item metadata collection to verified. collection.verified = true; // In the case of a sized collection, update the size on the collection parent. if collection_metadata.collection_details.is_some() { increment_collection_size(&mut collection_metadata, collection_metadata_info)?; } } None => return Err(MetadataError::CollectionNotFound.into()), }; /// Updating Metadata: // Reserialize metadata. clean_write_metadata(&mut metadata, ctx.accounts.metadata_info) }
 
assert_collection_verify_is_valid checks the collection mint matches the one recorded in the NFT mint’s metadata. And it checks master edition is derived from metaplex protocol. It also checks the token standard in collection mint’s metadata.
/// --- programs/token-metadata/program/src/assertions/collection.rs --- pub fn assert_collection_verify_is_valid( member_collection: &Option<Collection>, collection_metadata: &Metadata, collection_mint: &AccountInfo, edition_account_info: &AccountInfo, ) -> Result<(), ProgramError> { match member_collection { Some(collection) => { if collection.key != *collection_mint.key || collection_metadata.mint != *collection_mint.key { return Err(MetadataError::CollectionNotFound.into()); } } None => { return Err(MetadataError::CollectionNotFound.into()); } } match collection_metadata.edition_nonce { Some(nonce) => assert_derivation_with_bump( &crate::ID, edition_account_info, &[ PREFIX.as_bytes(), crate::ID.as_ref(), collection_metadata.mint.as_ref(), EDITION.as_bytes(), &[nonce], ], ), None => { let _ = assert_derivation( &crate::ID, edition_account_info, &[ PREFIX.as_bytes(), crate::ID.as_ref(), collection_metadata.mint.as_ref(), EDITION.as_bytes(), ], )?; Ok(()) } } .map_err(|_| MetadataError::CollectionMasterEditionAccountInvalid)?; assert_master_edition(collection_metadata, edition_account_info)?; Ok(()) } pub fn assert_master_edition( collection_data: &Metadata, edition_account_info: &AccountInfo, ) -> Result<(), ProgramError> { let edition = MasterEditionV2::from_account_info(edition_account_info) .map_err(|_err: ProgramError| MetadataError::CollectionMustBeAUniqueMasterEdition)?; match collection_data.token_standard { Some(TokenStandard::NonFungible) | Some(TokenStandard::ProgrammableNonFungible) => (), _ => return Err(MetadataError::CollectionMustBeAUniqueMasterEdition.into()), } if edition.max_supply != Some(0) { return Err(MetadataError::CollectionMustBeAUniqueMasterEdition.into()); } Ok(()) }

Unverify

Collection authority can use Verify instruction to set the collection.verified field of NFT to be false.
pub enum MetadataInstruction { /// ... /// Unverifies that an asset was created by a specific creator or belongs in an specified collection. /// /// Depending on the type of verification (e.g., creator or collection), additional accounts /// are required. #[account(0, signer, name="authority", desc="Creator to verify, collection (or metadata if parent burned) update authority or delegate")] #[account(1, optional, name="delegate_record", desc="Delegate record PDA")] #[account(2, writable, name="metadata", desc="Metadata account")] #[account(3, optional, name="collection_mint", desc="Mint of the Collection")] #[account(4, optional, writable, name="collection_metadata", desc="Metadata Account of the Collection")] #[account(5, name="system_program", desc="System program")] #[account(6, name="sysvar_instructions", desc="Instructions sysvar account")] Unverify(VerificationArgs), /// ... }

Delegate

Metaplex supports to allow collection to create delegate which has authority to verify NFT on behalf of the collection.
We can use instruction MetadataInstruction::Delegate to perform delegate.
/// --- programs/token-metadata/program/src/instruction/mod.rs --- /// Creates a delegate for an asset. /// /// A delegate has a role associated, which determines what actions the delegate can perform. There are /// two types of delegate: /// 1. Persistent delegate: only one delegate can exist at the same time for `Transfer`, `Sale` and /// `Utility` actions (pda of ["metadata", program id, mint id, "persistent_delegate", token owner id]) /// 2. Multiple delegates: for `Authority`, `Collection`, `Update` and `Uses` actions (pda of ["metadata", /// program id, mint id, role, update authority id, delegate owner id]) #[account(0, optional, writable, name="delegate_record", desc="Delegate record account")] #[account(1, name="delegate", desc="Owner of the delegated account")] #[account(2, writable, name="metadata", desc="Metadata account")] #[account(3, optional, name="master_edition", desc="Master Edition account")] #[account(4, optional, writable, name="token_record", desc="Token record account")] #[account(5, name="mint", desc="Mint of metadata")] #[account(6, optional, writable, name="token", desc="Token account of mint")] #[account(7, signer, name="authority", desc="Update authority or token owner")] #[account(8, signer, writable, name="payer", desc="Payer")] #[account(9, name="system_program", desc="System Program")] #[account(10, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(11, optional, name="spl_token_program", desc="SPL Token Program")] #[account(12, optional, name="authorization_rules_program", desc="Token Authorization Rules Program")] #[account(13, optional, name="authorization_rules", desc="Token Authorization Rules account")] Delegate(DelegateArgs)
 
MetadataInstruction::Delegate uses delegate to execute delegate action. We can use the args argument to specify the kind of delegation. To perform collection delegation, we need to specify the args to as CollectionV1.
Inside the function, it matches the args, for collection delegation, it will use create_other_delegate_v1 to perform logic.
/// --- programs/token-metadata/program/src/processor/delegate/delegate.rs --- /// Delegates an action over an asset to a specific account. pub fn delegate<'a>( program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], args: DelegateArgs, ) -> ProgramResult { let context = Delegate::to_context(accounts)?; // checks if it is a TokenDelegate creation let delegate_args = match &args { // Sale DelegateArgs::SaleV1 { amount, authorization_data, } => Some((TokenDelegateRole::Sale, amount, authorization_data)), // Transfer DelegateArgs::TransferV1 { amount, authorization_data, } => Some((TokenDelegateRole::Transfer, amount, authorization_data)), // Utility DelegateArgs::UtilityV1 { amount, authorization_data, } => Some((TokenDelegateRole::Utility, amount, authorization_data)), // Staking DelegateArgs::StakingV1 { amount, authorization_data, } => Some((TokenDelegateRole::Staking, amount, authorization_data)), // Standard DelegateArgs::StandardV1 { amount } => Some((TokenDelegateRole::Standard, amount, &None)), // LockedTransfer DelegateArgs::LockedTransferV1 { amount, authorization_data, .. } => Some(( TokenDelegateRole::LockedTransfer, amount, authorization_data, )), // we don't need to fail if did not find a match at this point _ => None, }; if let Some((role, amount, authorization_data)) = delegate_args { // proceed with the delegate creation if we have a match return create_persistent_delegate_v1( program_id, context, &args, role, *amount, authorization_data, ); } // checks if it is a MetadataDelegate creation let delegate_args = match &args { DelegateArgs::CollectionV1 { authorization_data } => { Some((MetadataDelegateRole::Collection, authorization_data)) } DelegateArgs::DataV1 { authorization_data } => { Some((MetadataDelegateRole::Data, authorization_data)) } DelegateArgs::ProgrammableConfigV1 { authorization_data } => { Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) } DelegateArgs::AuthorityItemV1 { authorization_data } => { Some((MetadataDelegateRole::AuthorityItem, authorization_data)) } DelegateArgs::DataItemV1 { authorization_data } => { Some((MetadataDelegateRole::DataItem, authorization_data)) } DelegateArgs::CollectionItemV1 { authorization_data } => { Some((MetadataDelegateRole::CollectionItem, authorization_data)) } DelegateArgs::ProgrammableConfigItemV1 { authorization_data } => Some(( MetadataDelegateRole::ProgrammableConfigItem, authorization_data, )), // we don't need to fail if did not find a match at this point _ => None, }; if let Some((role, _authorization_data)) = delegate_args { return create_other_delegate_v1(program_id, context, DelegateScenario::Metadata(role)); } // checks if it is a HolderDelegate creation let delegate_args = match &args { DelegateArgs::PrintDelegateV1 { authorization_data } => { Some((HolderDelegateRole::PrintDelegate, authorization_data)) } // we don't need to fail if did not find a match at this point _ => None, }; if let Some((role, _authorization_data)) = delegate_args { return create_other_delegate_v1(program_id, context, DelegateScenario::Holder(role)); } // this only happens if we did not find a match Err(MetadataError::InvalidDelegateArgs.into()) } /// --- programs/token-metadata/program/src/instruction/delegate.rs --- /// Delegate args can specify Metadata delegates and Token delegates. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum DelegateArgs { CollectionV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, SaleV1 { amount: u64, /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, TransferV1 { amount: u64, /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, DataV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, UtilityV1 { amount: u64, /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, StakingV1 { amount: u64, /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, StandardV1 { amount: u64, }, LockedTransferV1 { amount: u64, #[deprecated( since = "1.13.2", note = "The locked address is deprecated and will soon be removed." )] /// locked destination pubkey locked_address: Pubkey, /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, ProgrammableConfigV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, AuthorityItemV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, DataItemV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, CollectionItemV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, ProgrammableConfigItemV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, PrintDelegateV1 { /// Required authorization data to validate the request. authorization_data: Option<AuthorizationData>, }, }
 
create_other_delegate_v1 handles delegate.
It basically checks the authority of delegate, and creates and initializes corresponding pda to record the delegate information.
 
Steps:
  1. Signer check
    1. Check payer and authority (update authority of collection mint’s metadata account) has signed.
  1. Account ownership check
      • Check metadata account is owned by metaplex program.
      • Check collection mint is owned by SPL token program.
  1. Account address check
    1. Check system program and sysvar program account have correct address.
  1. Mint account check
    1. Check collection mint account matches mint account refered by the metadata account.
  1. Metadata update authority check
    1. Check update authority of metadata account has signed.
  1. Create pda account to record delegate
/// --- programs/token-metadata/program/src/processor/delegate/delegate.rs --- /// Creates a `DelegateRole::Collection` delegate. /// /// There can be multiple collections delegates set at any time. fn create_other_delegate_v1( program_id: &Pubkey, ctx: Context<Delegate>, delegate_scenario: DelegateScenario, ) -> ProgramResult { /// Signer check // signers assert_signer(ctx.accounts.payer_info)?; assert_signer(ctx.accounts.authority_info)?; /// Account ownership check // ownership assert_owned_by(ctx.accounts.metadata_info, program_id)?; assert_owner_in(ctx.accounts.mint_info, &SPL_TOKEN_PROGRAM_IDS)?; /// Account address check // key match assert_keys_equal(ctx.accounts.system_program_info.key, &system_program::ID)?; assert_keys_equal( ctx.accounts.sysvar_instructions_info.key, &sysvar::instructions::ID, )?; /// Mint account check // account relationships let metadata = Metadata::from_account_info(ctx.accounts.metadata_info)?; if metadata.mint != *ctx.accounts.mint_info.key { return Err(MetadataError::MintMismatch.into()); } match delegate_scenario { /// Metadata update authority check DelegateScenario::Metadata(_) => { // authority must match update authority assert_update_authority_is_correct(&metadata, ctx.accounts.authority_info)?; } DelegateScenario::Holder(_) => { // retrieving required optional account let token_info = match ctx.accounts.token_info { Some(token_info) => token_info, None => { return Err(MetadataError::MissingTokenAccount.into()); } }; // Check if the owner is accurate the token accounts are correct. assert_holding_amount( program_id, ctx.accounts.authority_info, ctx.accounts.metadata_info, &metadata, ctx.accounts.mint_info, token_info, 1, )?; } _ => return Err(MetadataError::InvalidDelegateRole.into()), } let delegate_record_info = match ctx.accounts.delegate_record_info { Some(delegate_record_info) => delegate_record_info, None => { return Err(MetadataError::MissingDelegateRecord.into()); } }; /// Create pda account to record delegate // process the delegation creation (the derivation is checked // by the create helper) create_pda_account( program_id, delegate_record_info, ctx.accounts.delegate_info, ctx.accounts.mint_info, ctx.accounts.authority_info, ctx.accounts.payer_info, ctx.accounts.system_program_info, delegate_scenario, ) }
 
create_pda_account calculates delegate_record address and check whether it matches the passed in address. Then it creates delegate_record pda to record delegate information.
The delegate information is essentially recorded using map with key combination:
  • collection mint account
  • delegate role
  • authority of metadata account of collection mint
  • delegate address
If some program wants to check whether an account is delegate of certain mint, it can just calcualtes the corresponding delegate_record address and check the data stored inside. Note that, even the update authority of the collection mint metadata account changes, the previous delegate_record still exsits.
/// --- programs/token-metadata/program/src/processor/delegate/delegate.rs --- #[allow(clippy::too_many_arguments)] fn create_pda_account<'a>( program_id: &Pubkey, delegate_record_info: &'a AccountInfo<'a>, delegate_info: &'a AccountInfo<'a>, mint_info: &'a AccountInfo<'a>, authority_info: &'a AccountInfo<'a>, payer_info: &'a AccountInfo<'a>, system_program_info: &'a AccountInfo<'a>, delegate_scenario: DelegateScenario, ) -> ProgramResult { // validates the delegate derivation let delegate_role = match delegate_scenario { DelegateScenario::Metadata(role) => role.to_string(), DelegateScenario::Holder(role) => role.to_string(), _ => return Err(MetadataError::InvalidDelegateRole.into()), }; let mut signer_seeds = vec![ PREFIX.as_bytes(), program_id.as_ref(), mint_info.key.as_ref(), delegate_role.as_bytes(), authority_info.key.as_ref(), delegate_info.key.as_ref(), ]; let bump = &[assert_derivation( program_id, delegate_record_info, &signer_seeds, )?]; signer_seeds.push(bump); if !delegate_record_info.data_is_empty() { return Err(MetadataError::DelegateAlreadyExists.into()); } // allocate the delegate account create_or_allocate_account_raw( *program_id, delegate_record_info, system_program_info, payer_info, MetadataDelegateRecord::size(), &signer_seeds, )?; match delegate_scenario { DelegateScenario::Metadata(_) => { let pda = MetadataDelegateRecord { bump: bump[0], mint: *mint_info.key, delegate: *delegate_info.key, update_authority: *authority_info.key, ..Default::default() }; borsh::to_writer(&mut delegate_record_info.try_borrow_mut_data()?[..], &pda)?; } DelegateScenario::Holder(_) => { let pda = HolderDelegateRecord { bump: bump[0], mint: *mint_info.key, delegate: *delegate_info.key, update_authority: *authority_info.key, ..Default::default() }; borsh::to_writer(&mut delegate_record_info.try_borrow_mut_data()?[..], &pda)?; } _ => return Err(MetadataError::InvalidDelegateRole.into()), }; Ok(()) }

Appendix

Initialize Mint Close Authority

process_initialize_mint_close_authority initializes the close_authority extension. It uses unpack_uninitialized to get mint(PodStateWithExtensionsMut::<PodMint> ) which includes the base state (PodMint), account_type and tlv data. Then it calls init_extension get the mutable reference of MintCloseAuthority extension data in tlv. Finally it updates the close_authority field in the extension.
 
Detailed introduction about extension functionality in Token Program 2022 can be found in another blog.
// --- token/program-2022/src/processor.rs --- /// Processes an [InitializeMintCloseAuthority](enum.TokenInstruction.html) /// instruction pub fn process_initialize_mint_close_authority( accounts: &[AccountInfo], close_authority: PodCOption<Pubkey>, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let mint_account_info = next_account_info(account_info_iter)?; let mut mint_data = mint_account_info.data.borrow_mut(); let mut mint = PodStateWithExtensionsMut::<PodMint>::unpack_uninitialized(&mut mint_data)?; let extension = mint.init_extension::<MintCloseAuthority>(true)?; extension.close_authority = close_authority.try_into()?; Ok(()) }

Revoke

MetadataInstruction::Revoke is to revoke delegate. It requires signature of payer and collection mint metadata’s authority. It will close old delegate record account and transfer sol to payer.
/// --- programs/token-metadata/program/src/instruction/mod.rs --- /// Revokes a delegate. /// /// A delegate can revoke itself by signing the transaction as the 'approver'. #[account(0, optional, writable, name="delegate_record", desc="Delegate record account")] #[account(1, name="delegate", desc="Owner of the delegated account")] #[account(2, writable, name="metadata", desc="Metadata account")] #[account(3, optional, name="master_edition", desc="Master Edition account")] #[account(4, optional, writable, name="token_record", desc="Token record account")] #[account(5, name="mint", desc="Mint of metadata")] #[account(6, optional, writable, name="token", desc="Token account of mint")] #[account(7, signer, name="authority", desc="Update authority or token owner")] #[account(8, signer, writable, name="payer", desc="Payer")] #[account(9, name="system_program", desc="System Program")] #[account(10, name="sysvar_instructions", desc="Instructions sysvar account")] #[account(11, optional, name="spl_token_program", desc="SPL Token Program")] #[account(12, optional, name="authorization_rules_program", desc="Token Authorization Rules Program")] #[account(13, optional, name="authorization_rules", desc="Token Authorization Rules account")] Revoke(RevokeArgs),
 
revoke performs revocation logic.
/// --- programs/token-metadata/program/src/processor/delegate/revoke.rs --- /// Revoke a delegation of the token. pub fn revoke<'a>( program_id: &Pubkey, accounts: &'a [AccountInfo<'a>], args: RevokeArgs, ) -> ProgramResult { let context = Revoke::to_context(accounts)?; // checks if it is a TokenDelegate creation let token_delegate = match &args { // Sale RevokeArgs::SaleV1 => Some(TokenDelegateRole::Sale), // Transfer RevokeArgs::TransferV1 => Some(TokenDelegateRole::Transfer), // Utility RevokeArgs::UtilityV1 => Some(TokenDelegateRole::Utility), // Staking RevokeArgs::StakingV1 => Some(TokenDelegateRole::Staking), // Standard RevokeArgs::StandardV1 => Some(TokenDelegateRole::Standard), // LockedTransfer RevokeArgs::LockedTransferV1 => Some(TokenDelegateRole::LockedTransfer), // Migration RevokeArgs::MigrationV1 => Some(TokenDelegateRole::Migration), // we don't need to fail if did not find a match at this point _ => None, }; if let Some(role) = token_delegate { // proceed with the delegate revoke if we have a match return revoke_persistent_delegate_v1(program_id, context, role); } // checks if it is a MetadataDelegate creation let metadata_delegate = match &args { RevokeArgs::CollectionV1 => Some(MetadataDelegateRole::Collection), RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::ProgrammableConfigV1 => Some(MetadataDelegateRole::ProgrammableConfig), RevokeArgs::AuthorityItemV1 => Some(MetadataDelegateRole::AuthorityItem), RevokeArgs::DataItemV1 => Some(MetadataDelegateRole::DataItem), RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), RevokeArgs::ProgrammableConfigItemV1 => Some(MetadataDelegateRole::ProgrammableConfigItem), // we don't need to fail if did not find a match at this point _ => None, }; if let Some(role) = metadata_delegate { return revoke_other_delegate_v1(program_id, context, DelegateScenario::Metadata(role)); } // checks if it is a HolderDelegate creation let holder_delegate = match &args { RevokeArgs::PrintDelegateV1 => Some(HolderDelegateRole::PrintDelegate), // we don't need to fail if did not find a match at this point _ => None, }; if let Some(role) = holder_delegate { return revoke_other_delegate_v1(program_id, context, DelegateScenario::Holder(role)); } // this only happens if we did not find a match Err(MetadataError::InvalidDelegateArgs.into()) }

Reference