This part details the step-by-step algorithm for implementing a Solidity Solang program for NFT minting and transfer, where the NFT mint authority is a Program Derived Address (PDA).
illustration
b
Create NFT Mint Account And Token MetaData Account

Step-by-Step Guide to Setting Up the Program and Writing the Constructor

  1. Create a New Project Folder:

    • Establish a new folder dedicated to your project.
  2. Initialize the Anchor Project with Solidity:

    • Run anchor init pda-mint-authority --solidity in the terminal within your project folder.
    • Wait for Anchor to install all necessary dependencies.
  3. Navigate to the Project Directory:

    • Enter the project directory using cd pda-mint-authority.
  4. Open the Project in Visual Studio Code (VSCode):

    • Use the command code . to open your project in VSCode.
  1. Incorporate Essential Libraries:

    • Add libraries such as SPL token, system instructions, and Solana.
  2. Edit and Configure anchor.toml File:

    • Modify the anchor.toml file according to your project requirements.
  3. Importing Required Libraries:

    • Start your Solidity file (e.g., pda-mint-authority.sol) by importing necessary libraries from the root folder:
 import "../libraries/spl_token.sol";
 import "solana";
  1. Define the Program ID:

    • Use the @program_id annotation to specify your contract's on-chain address:
 @program_id("4Huxs8ujxKqT76qqfmfJXMThmo4WMsvmdiCkqzNKF5kq")
  1. Initializing the Contract:
    • Declare your contract with the required properties:
contract pda_mint_authority {
 bytes1 bump; // stores the bump for the pda address

 @payer(payer)
 @seed("mint_authority") // hard-coded seed
 constructor(
 @bump bytes1 _bump  // bump for the pda address
 ) {
 (address pda, bytes1 pdaBump) = try_find_program_address(["mint_authority"], type(pda_mint_authority).program_id);
 require(pdaBump == _bump, 'INVALID_BUMP');

 bump = _bump;
 }
}
  1. Constructor Setup:

    • Implement the constructor using @payer, @seed, and @bump annotations to define the contract's initialization logic.
  2. PDA Derivation and Verification:

    • Use try_find_program_address to derive the PDA address and bump from the specified seed and program ID.
    • Ensure the provided bump in the constructor matches the derived bump for address validity:
 constructor(@bump bytes1 _bump) {
 (address pda, bytes1 pdaBump) = try_find_program_address(["mint_authority"], type(pda_mint_authority).program_id);
 require(pdaBump == _bump, 'INVALID_BUMP');
 bump = _bump;
 }
  1. try_find_program_address Function:

    • This function is crucial for generating a Program Derived Address (PDA). It computes a PDA using provided seeds and a program ID, ensuring that the derived address is unique and secure.
  2. Solidity require Check:

    • The require statement validates that the bump used to generate the PDA is consistent each time the constructor is invoked, ensuring the uniqueness and security of the PDA.
  3. Role of @payer and @seed Annotations:

    • The @payer annotation specifies the account responsible for transaction fees and contract creation.
    • The @seed annotation, particularly "mint_authority" in this case, is used to derive the PDA and verify its authenticity.
  1. Adding the Function in Solidity:

    • After the constructor, define the createTokenMint function to handle mint creation:
 @mutableSigner(payer)
 @mutableSigner(mint)
 @mutableAccount(metadata)
 // Additional account annotations
 function createTokenMint(/* parameters */) external {
 // Function logic
 }
  1. Setting Up Accounts and Parameters:

    • The function uses several annotated accounts, each with specific roles and privileges:
    • @mutableSigner(payer): The account that pays for the transaction.
    • @mutableSigner(mint): The account where the minted token will be created.
    • @mutableAccount(metadata): The account for storing token metadata.
  2. Defining Function Arguments:

    • createTokenMint accepts parameters for setting up the mint account:
    • address freezeAuthority: The authority that can freeze the mint account.
    • uint8 decimals: The decimal precision for the token.
    • string name, symbol, uri: Attributes for the metadata account.
  1. Invoke System and Token Programs:

    • Utilize the SplToken.create_mint function to establish the mint account and set attributes like mint authority, freeze authority, and decimals.
    • Example of invoking this function:
 SplToken.create_mint(
 tx.accounts.payer.key, // Payer account
 tx.accounts.mint.key,  // Mint account
 tx.accounts.mintAuthority.key, // Mint authority
 freezeAuthority,       // Freeze authority
 decimals               // Decimals
 );
  1. Invoke Metadata Program:
  • Use a custom function _createMetadataAccount to create the metadata account:
 _createMetadataAccount(
 tx.accounts.metadata.key,       // Metadata account
 tx.accounts.mint.key,           // Mint account
 tx.accounts.mintAuthority.key,  // Mint authority
 // Additional parameters
 );
  1. Breaking Down the Function Logic:
    • The createTokenMint function first sets up the mint account using the SPL token library, then creates the metadata account.
    • It takes into account various parameters and accounts, ensuring correct setup for both minting the token and establishing its metadata.

The createTokenMint function should look like this:

 @mutableSigner(payer)
 @mutableSigner(mint)
 @mutableAccount(metadata)
 @account(mintAuthority)
 @account(rentAddress)
 @account(metaplexId)
 function createTokenMint(
 address freezeAuthority,
 uint8 decimals,
 string name,
 string symbol,
 string uri
 ) external {
 SplToken.create_mint(
 tx.accounts.payer.key,
 tx.accounts.mint.key,
 tx.accounts.mintAuthority.key,
 freezeAuthority,
 decimals
 );

 _createMetadataAccount(
 tx.accounts.metadata.key,
 tx.accounts.mint.key,
 tx.accounts.mintAuthority.key,
 tx.accounts.payer.key,
 tx.accounts.payer.key,
 name,
 symbol,
 uri,
 tx.accounts.rentAddress.key,
 tx.accounts.metaplexId.key
 );
 }
  • @mutableSigner and @mutableAccount: Indicate accounts that will be mutated or changed during the transaction.
  • @account: Used for accounts that are part of the transaction but not necessarily mutated.
  • Objective: To build a Solana SPL NFT minter program using Program Derived Addresses (PDAs) as mint authorities.

  • Approach: Implementing PDAs requires customizing certain library methods, particularly from the MPL (Metaplex) and SPL (Solana token program) library.

  • Defining the _createMetadataAccount Method

  • Function Purpose: To create a metadata account for the minted token, signed with a PDA as the mint authority.

  • Method Declaration: The method is defined as a private function within the contract:

 // Create metadata account, must reimplement manually to sign with PDA
 function _createMetadataAccount(
 address metadata,
 address mint,
 address mintAuthority,
 address payer,
 address updateAuthority,
 string name,
 string symbol,
 string uri,
 address rentAddress,
 address metaplexId
 ) private {
 // Implementation details
 }
  1. Arguments of the Method:

    • The method takes various parameters, mainly address and string types:
      • address metadata: Address for the token's metadata account.
      • address mint: Address of the mint account for the token.
      • address mintAuthority: The mint authority, represented by a PDA.
      • Additional parameters for the payer, update authority, name, symbol, uri, rent address, and metaplexId.
  2. Deriving the PDA:

  • Utilizes try_find_program_address to derive the PDA from seeds and the program ID:
 (address pda, bytes1 _bump) = try_find_program_address(["mint_authority"], type(pda_mint_authority).program_id);
 require(mintAuthority == pda, 'INVALID_PDA');
  1. Setting Up Data Structures:
  • Initializes DataV2 and CreateMetadataAccountArgsV3 structs with the provided token attributes.
 DataV2 data = DataV2({
 name: name,
 symbol: symbol,
 uri: uri,
 sellerFeeBasisPoints: 0,
 creatorsPresent: false,
 collectionPresent: false,
 usesPresent: false
 });

 CreateMetadataAccountArgsV3 args = CreateMetadataAccountArgsV3({
 data: data,
 isMutable: true,
 collectionDetailsPresent: false
 });
  1. Account Metadata Configuration:

    • Configures AccountMeta array with necessary accounts for the transaction:

    • Accounts include metadata, mint, mintAuthority, payer, updateAuthority, system program, and rent account.

 AccountMeta[7] metas = [
 AccountMeta({pubkey: metadata, is_writable: true, is_signer: false}),
 AccountMeta({pubkey: mint, is_writable: false, is_signer: false}),
 AccountMeta({pubkey: mintAuthority, is_writable: false, is_signer: true}),
 AccountMeta({pubkey: payer, is_writable: true, is_signer: true}),
 AccountMeta({pubkey: updateAuthority, is_writable: false, is_signer: false}),
 AccountMeta({pubkey: address"11111111111111111111111111111111", is_writable: false, is_signer: false}),
 AccountMeta({pubkey: rentAddress, is_writable: false, is_signer: false})
 ];
  1. Encoding Instruction Data:

    • Encodes the instruction data using abi.encode, including the discriminator and arguments.
 bytes1 discriminator = 33;
 bytes instructionData = abi.encode(discriminator, args);
  1. Cross-Program Invocation (CPI):
    • Performs a CPI to the Metaplex program, passing in the encoded instruction data and account metadata:
 metaplexId.call{accounts: metas, seeds: [["mint_authority", abi.encode(_bump)]]}(instructionData);
  • try_find_program_address Function: Generates a PDA from given seeds and the program ID.
  • Data Structuring: Essential for organizing the metadata attributes and CPI arguments.
  • ABI Encoding: Translates function arguments into a format compatible with Solana's program calls.
  • CPI to Metaplex: Executes the logic to create the metadata account, utilizing the PDA for authorization.

Formulating Solidity Structs and Metadata Account Creation Process

Creating Solidity Structs for NFT Metadata

  1. Defining Structs:

    • Implement two key structs, DataV2 and CreateMetadataAccountArgsV3, to organize NFT metadata and account creation arguments:
 struct CreateMetadataAccountArgsV3 {
 DataV2 data;
 bool isMutable;
 bool collectionDetailsPresent;
 }

 struct DataV2 {
 string name;
 string symbol;
 string uri;
 uint16 sellerFeeBasisPoints;
 bool creatorsPresent;
 bool collectionPresent;
 bool usesPresent;
 }
  1. Initializing Structs:

    • Initialize these structs with the required data for the NFT:
 DataV2 data = DataV2({
 name: name,
 symbol: symbol,
 uri: uri,
 sellerFeeBasisPoints: 0,
 creatorsPresent: false,
 collectionPresent: false,
 usesPresent: false
 });

 CreateMetadataAccountArgsV3 args = CreateMetadataAccountArgsV3({
 data: data,
 isMutable: true,
 collectionDetailsPresent: false
 });

Account Metadata for the Instruction

  1. Configuring Account Metadata:

    • Set up AccountMeta array to define accounts involved in the transaction:
 AccountMeta[7] metas = [
 AccountMeta({pubkey: metadata, is_writable: true, is_signer: false}),
 // Additional AccountMeta configurations
 ];
  1. Role of Mint Authority (PDA):

    • The mintAuthority account, represented by a PDA, plays a crucial role in signing the transaction.
  1. Encoding Instruction Data:

    • Use ABI encoding to format the instruction data, including a discriminator for Solana:
 bytes1 discriminator = 33;
 bytes instructionData = abi.encode(discriminator, args);
  1. Cross-Program Invocation to Metaplex:
  • Perform a Cross-Program Invocation (CPI) to the Metaplex program, passing in the encoded data and account metadata:
 metaplexId.call{accounts: metas, seeds: [["mint_authority", abi.encode(_bump)]]}(instructionData);
  • Structs DataV2 and CreateMetadataAccountArgsV3: Used to structure and manage NFT metadata information.
  • Account Metadata (AccountMeta) Configuration: Determines the roles and permissions of each account in the transaction.
  • ABI Encoding: Translates Solidity arguments into a format compatible with Solana's program calls.
  • CPI to Metaplex: Executes the logic for metadata account creation, using the PDA for authorization.

By integrating these elements, the Solidity Solang program effectively creates a mint account for NFTs and sets up a corresponding metadata account. The use of Program Derived Addresses (PDAs) as mint authorities adds an extra layer of security and control to the minting process. This approach aligns with Solana's programming model, ensuring the NFT minted is authenticated and managed correctly.