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.
Metadata
Fields:
- key:
This field identifies the type of account. The
Key
enum distinguishes between different types of metadata and related accounts within the Metaplex program, - update_authority
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.- mint
public key of the mint account(collection or NFT mint account).
- data
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 ofCreator
structs that lists the creators of the NFT, potentially with share allocations.
encapsulates the core attributes of the NFT, including:
- primary_sale_happened
A flag indicating whether the NFT has been sold in a primary sale. Once set to
true
, any subsequent sales are considered secondary sales.- is_mutable
This flag determines whether the metadata can be updated. If
false
, the metadata becomes immutable and cannot be changed.- edition_nonce
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.
- token_standard
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
andProgrammableNonFungibleEdition
: NFTs with programmable configurations that allow additional functionality.
specifies the type of token:
- collection
The
Collection
struct links the NFT to a collection, indicating whether the collection has been verified and providing the collection’s public key.- uses
use_method: UseMethod
: The method of use.remaining: u64
: The number of uses left.total: u64
: The total number of uses available.
struct defines how the NFT can be used, including:
- collection_details
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
.- 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:
- Extract Arguments
Extracts the arguments from
CreateArgs
which includes asset_data
, decimals
, and print_supply
.- Token Standard Validation
Ensures that the token standard is not
NonFungibleEdition
or ProgrammableNonFungibleEdition
as these are invalid for this instruction.- Levy Fees
Charges fees to fund the metadata account with rent and the protocol fee.
- 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).
- Metadata Account Creation
Creates the metadata account using the
process_create_metadata_accounts_logic
function, which takes various account infos and arguments related to the asset.- 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 thecreate_master_edition
function. - For pNFTs, stores the token standard value at the end of the master edition account data.
- 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.
- Set Fee Flag
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:
- Check for SPL Token 2022
Checks if the SPL Token program being used is SPL Token 2022.
- Calculate Mint Account Size
Determines the size of the mint account based on whether SPL Token 2022 is used and includes the necessary extensions.
- Create Mint Account
Invokes the system instruction to create the account, specifying the payer, the mint account, the required balance, the size, and the program ID.
- Initialize Extensions for SPL Token 2022
If SPL Token 2022 is used, initializes the
MintCloseAuthority
and MetadataPointer
extensions for the mint account.- Set Decimals Based on Token Standard
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.
- Initialize the Mint Account
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:
- Require
print_supply
is provided.
- Create master edition account.
- If the mint is
pNFT
, then addTokenStandard
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:
- key
type of the edition account
- supply
current supply of the print NFT.
- max_supply
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 ofx
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 ofx
editions.
max supply of print NFT.
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:
- Deserialize Metadata
Deserialize Metadata Stored In Metadata Account. Also check that the Metadata account belongs to metaplex program.
- Deserialize MINT data
Deserialize MINT data stored in MINT account.
- Check Master Edition Account Is Derived From Metaplex Program
So that Metaplex Program can successfully create and manage the Master Edition Account.
- Check Validity of Passed-in SPL Token Program
Check it is one of the official SPL token programs.
- Check passed-in Mint Authority
Check it is the owner of the MINT account, and it is signer.
- Check Metadata Account is Owned by Metaplex program
- Check Mint Account is owned by SPL Token Program
- Check Metadata Account’s Mint matches Mint Account
- Check Mint Account’s Decimal is Zero
- 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.
- Check Mint Account Supply not Larger Than 1
- Create Master Edition Account
allocate space and assign owner to metaplex program.
- Update Master Edition Data
- Update Metadata Account Data
Set token standard to
TokenStandard::NonFungible
- 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:
- Argument Destructuring and Validation
- The
amount
to be minted is extracted from theargs
. - The function immediately returns an error if
amount
is0
, as minting zero tokens is not allowed.
- Signer Validation
- checks that both the
authority_info
andpayer_info
accounts are signers. This ensures that the minting operation is authorized and that the payer is providing the necessary resources.
- 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.
- Metadata and Mint Matching
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.- 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.
- Unpack Mint data
The
mint_info
account is unpacked to retrieve the mint details.- 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.
- Token Account Initialization
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.- Token Record Handling (for Programmable NFTs)
For Programmable NFTs (pNFTs), the function ensures the existence and validity of the token record account.
- 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.
- 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:- it has argumenet
collection
which refers the collection token address,collection
helps group collection NFTs together.
- it doesn’t have
collectionDetails
argument because thisi is a NFT andcollectionDetails
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:
- 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 usingassert_owned_by
. - Similar ownership checks are performed on
collection_mint_info
,collection_metadata_info
, andcollection_master_edition_info
.
- Loading Metadata:
- The function then deserializes the metadata from
metadata_info
andcollection_metadata_info
accounts. - It checks if the collection has already been verified to avoid redundant operations.
- Collection Validation:
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
.- Authority Validation:
- The function determines if the authority provided (in
authority_info
) is allowed to verify the collection. This is done usingAuthorityType::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.
- Collection Verification:
- If the NFT's collection field is present and not already verified, the function sets the
verified
field totrue
. - 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
.
- Updating Metadata:
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:
- Signer check
Check payer and authority (update authority of collection mint’s metadata account) has signed.
- Account ownership check
- Check metadata account is owned by metaplex program.
- Check collection mint is owned by SPL token program.
- Account address check
Check system program and sysvar program account have correct address.
- Mint account check
Check collection mint account matches mint account refered by the metadata account.
- Metadata update authority check
Check update authority of metadata account has signed.
- 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()) }