NFT Minting Contract

This contract implements a complete NFT collection with Metaplex standards, featuring minting limits, whitelist phases, and royalty enforcement.

Collection Architecture

Text
┌─────────────────────────────────────────────────────────────────┐
│                   NFT Collection Structure                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │               Collection Config (PDA)                   │    │
│  │  ─────────────────────────────────────────────────────  │    │
│  │  Authority        Creator                               │    │
│  │  Collection Mint  Master NFT                            │    │
│  │  Price            0.5 SOL                               │    │
│  │  Max Supply       10,000                                │    │
│  │  Items Minted     1,234                                 │    │
│  │  Start Time       1704067200                            │    │
│  │  WL Root          0xabc...                              │    │
│  └─────────────────────────────────────────────────────────┘    │
│                         │                                       │
│        ┌────────────────┼────────────────┐                      │
│        ▼                ▼                ▼                      │
│  ┌───────────┐    ┌───────────┐    ┌───────────┐                │
│  │  NFT #1   │    │  NFT #2   │    │  NFT #N   │                │
│  │  Mint     │    │  Mint     │    │  Mint     │                │
│  │  Metadata │    │  Metadata │    │  Metadata │                │
│  │  Master   │    │  Master   │    │  Master   │                │
│  └───────────┘    └───────────┘    └───────────┘                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Complete Implementation

Rust
use anchor_lang::prelude::*;
use anchor_lang::system_program;
use anchor_spl::token::{self, Token, TokenAccount, Mint, MintTo};
use anchor_spl::associated_token::AssociatedToken;
use anchor_spl::metadata::{
    create_master_edition_v3,
    create_metadata_accounts_v3,
    Metadata,
    CreateMasterEditionV3,
    CreateMetadataAccountsV3,
    mpl_token_metadata::types::{Creator, DataV2, Collection},
};
use solana_program::keccak;

declare_id!("NFT1111111111111111111111111111111111111111");

#[program]
pub mod nft_minting {
    use super::*;

    /// Initialize the NFT collection
    pub fn initialize_collection(
        ctx: Context<InitializeCollection>,
        name: String,
        symbol: String,
        uri: String,
        price: u64,
        max_supply: u32,
        start_time: i64,
        whitelist_price: u64,
        whitelist_end_time: i64,
        royalty_basis_points: u16,
    ) -> Result<()> {
        require!(max_supply > 0, NftError::InvalidMaxSupply);
        require!(price > 0, NftError::InvalidPrice);
        require!(royalty_basis_points <= 10000, NftError::InvalidRoyalty);

        let config = &mut ctx.accounts.config;
        config.authority = ctx.accounts.authority.key();
        config.collection_mint = ctx.accounts.collection_mint.key();
        config.treasury = ctx.accounts.treasury.key();
        config.name = name.clone();
        config.symbol = symbol.clone();
        config.base_uri = uri.clone();
        config.price = price;
        config.whitelist_price = whitelist_price;
        config.max_supply = max_supply;
        config.items_minted = 0;
        config.start_time = start_time;
        config.whitelist_end_time = whitelist_end_time;
        config.whitelist_root = [0u8; 32];
        config.royalty_basis_points = royalty_basis_points;
        config.paused = false;
        config.bump = ctx.bumps.config;

        // Create collection NFT
        let seeds = &[
            b"config",
            ctx.accounts.authority.key().as_ref(),
            &[config.bump],
        ];
        let signer_seeds = &[&seeds[..]];

        // Mint collection NFT
        let cpi_accounts = MintTo {
            mint: ctx.accounts.collection_mint.to_account_info(),
            to: ctx.accounts.collection_token.to_account_info(),
            authority: ctx.accounts.config.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
            signer_seeds,
        );
        token::mint_to(cpi_ctx, 1)?;

        // Create collection metadata
        let creators = vec![
            Creator {
                address: ctx.accounts.authority.key(),
                verified: false,
                share: 100,
            },
        ];

        let collection_data = DataV2 {
            name: name.clone(),
            symbol: symbol.clone(),
            uri: uri.clone(),
            seller_fee_basis_points: royalty_basis_points,
            creators: Some(creators),
            collection: None,
            uses: None,
        };

        create_metadata_accounts_v3(
            CpiContext::new_with_signer(
                ctx.accounts.metadata_program.to_account_info(),
                CreateMetadataAccountsV3 {
                    metadata: ctx.accounts.collection_metadata.to_account_info(),
                    mint: ctx.accounts.collection_mint.to_account_info(),
                    mint_authority: ctx.accounts.config.to_account_info(),
                    payer: ctx.accounts.authority.to_account_info(),
                    update_authority: ctx.accounts.config.to_account_info(),
                    system_program: ctx.accounts.system_program.to_account_info(),
                    rent: ctx.accounts.rent.to_account_info(),
                },
                signer_seeds,
            ),
            collection_data,
            true,
            true,
            None,
        )?;

        // Create master edition
        create_master_edition_v3(
            CpiContext::new_with_signer(
                ctx.accounts.metadata_program.to_account_info(),
                CreateMasterEditionV3 {
                    edition: ctx.accounts.collection_master_edition.to_account_info(),
                    mint: ctx.accounts.collection_mint.to_account_info(),
                    update_authority: ctx.accounts.config.to_account_info(),
                    mint_authority: ctx.accounts.config.to_account_info(),
                    payer: ctx.accounts.authority.to_account_info(),
                    metadata: ctx.accounts.collection_metadata.to_account_info(),
                    token_program: ctx.accounts.token_program.to_account_info(),
                    system_program: ctx.accounts.system_program.to_account_info(),
                    rent: ctx.accounts.rent.to_account_info(),
                },
                signer_seeds,
            ),
            Some(0), // 0 = non-fungible (no editions allowed)
        )?;

        emit!(CollectionInitialized {
            config: ctx.accounts.config.key(),
            collection_mint: config.collection_mint,
            max_supply,
            price,
        });

        Ok(())
    }

    /// Set whitelist merkle root
    pub fn set_whitelist_root(
        ctx: Context<UpdateConfig>,
        root: [u8; 32],
    ) -> Result<()> {
        ctx.accounts.config.whitelist_root = root;
        Ok(())
    }

    /// Mint an NFT (public or whitelist)
    pub fn mint_nft(
        ctx: Context<MintNft>,
        name: String,
        uri: String,
        whitelist_proof: Option<Vec<[u8; 32]>>,
    ) -> Result<()> {
        let config = &ctx.accounts.config;
        let clock = Clock::get()?;

        require!(!config.paused, NftError::MintingPaused);
        require!(config.items_minted < config.max_supply, NftError::SoldOut);

        // Determine price and verify access
        let (price, is_whitelist) = if clock.unix_timestamp < config.whitelist_end_time {
            // Whitelist phase
            if let Some(proof) = whitelist_proof {
                let leaf = keccak::hashv(&[ctx.accounts.minter.key().as_ref()]);
                require!(
                    verify_proof(&proof, &config.whitelist_root, &leaf.0),
                    NftError::NotWhitelisted
                );
                (config.whitelist_price, true)
            } else {
                return Err(NftError::NotWhitelisted.into());
            }
        } else {
            // Public phase
            require!(
                clock.unix_timestamp >= config.start_time,
                NftError::MintNotStarted
            );
            (config.price, false)
        };

        // Check per-wallet limit
        let mint_tracker = &mut ctx.accounts.mint_tracker;
        let max_per_wallet = if is_whitelist { 2 } else { 5 };
        require!(
            mint_tracker.count < max_per_wallet,
            NftError::WalletLimitExceeded
        );

        // Transfer payment
        system_program::transfer(
            CpiContext::new(
                ctx.accounts.system_program.to_account_info(),
                system_program::Transfer {
                    from: ctx.accounts.minter.to_account_info(),
                    to: ctx.accounts.treasury.to_account_info(),
                },
            ),
            price,
        )?;

        // Mint NFT token
        let seeds = &[
            b"config",
            config.authority.as_ref(),
            &[config.bump],
        ];
        let signer_seeds = &[&seeds[..]];

        let cpi_accounts = MintTo {
            mint: ctx.accounts.nft_mint.to_account_info(),
            to: ctx.accounts.nft_token.to_account_info(),
            authority: ctx.accounts.config.to_account_info(),
        };
        let cpi_ctx = CpiContext::new_with_signer(
            ctx.accounts.token_program.to_account_info(),
            cpi_accounts,
            signer_seeds,
        );
        token::mint_to(cpi_ctx, 1)?;

        // Create metadata
        let nft_number = config.items_minted + 1;
        let creators = vec![
            Creator {
                address: config.authority,
                verified: false,
                share: 100,
            },
        ];

        let metadata_data = DataV2 {
            name,
            symbol: config.symbol.clone(),
            uri,
            seller_fee_basis_points: config.royalty_basis_points,
            creators: Some(creators),
            collection: Some(Collection {
                verified: false,
                key: config.collection_mint,
            }),
            uses: None,
        };

        create_metadata_accounts_v3(
            CpiContext::new_with_signer(
                ctx.accounts.metadata_program.to_account_info(),
                CreateMetadataAccountsV3 {
                    metadata: ctx.accounts.nft_metadata.to_account_info(),
                    mint: ctx.accounts.nft_mint.to_account_info(),
                    mint_authority: ctx.accounts.config.to_account_info(),
                    payer: ctx.accounts.minter.to_account_info(),
                    update_authority: ctx.accounts.config.to_account_info(),
                    system_program: ctx.accounts.system_program.to_account_info(),
                    rent: ctx.accounts.rent.to_account_info(),
                },
                signer_seeds,
            ),
            metadata_data,
            true,
            true,
            None,
        )?;

        // Create master edition
        create_master_edition_v3(
            CpiContext::new_with_signer(
                ctx.accounts.metadata_program.to_account_info(),
                CreateMasterEditionV3 {
                    edition: ctx.accounts.nft_master_edition.to_account_info(),
                    mint: ctx.accounts.nft_mint.to_account_info(),
                    update_authority: ctx.accounts.config.to_account_info(),
                    mint_authority: ctx.accounts.config.to_account_info(),
                    payer: ctx.accounts.minter.to_account_info(),
                    metadata: ctx.accounts.nft_metadata.to_account_info(),
                    token_program: ctx.accounts.token_program.to_account_info(),
                    system_program: ctx.accounts.system_program.to_account_info(),
                    rent: ctx.accounts.rent.to_account_info(),
                },
                signer_seeds,
            ),
            Some(0),
        )?;

        // Update state
        let config = &mut ctx.accounts.config;
        config.items_minted += 1;

        let mint_tracker = &mut ctx.accounts.mint_tracker;
        mint_tracker.count += 1;

        emit!(NftMinted {
            config: ctx.accounts.config.key(),
            mint: ctx.accounts.nft_mint.key(),
            minter: ctx.accounts.minter.key(),
            number: nft_number,
            is_whitelist,
        });

        Ok(())
    }

    /// Update collection configuration
    pub fn update_config(
        ctx: Context<UpdateConfig>,
        new_price: Option<u64>,
        new_start_time: Option<i64>,
        new_base_uri: Option<String>,
    ) -> Result<()> {
        let config = &mut ctx.accounts.config;

        if let Some(price) = new_price {
            config.price = price;
        }
        if let Some(time) = new_start_time {
            config.start_time = time;
        }
        if let Some(uri) = new_base_uri {
            config.base_uri = uri;
        }

        Ok(())
    }

    /// Pause/unpause minting
    pub fn set_paused(ctx: Context<UpdateConfig>, paused: bool) -> Result<()> {
        ctx.accounts.config.paused = paused;
        Ok(())
    }

    /// Withdraw funds from treasury
    pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
        **ctx.accounts.treasury.to_account_info().try_borrow_mut_lamports()? -= amount;
        **ctx.accounts.authority.to_account_info().try_borrow_mut_lamports()? += amount;
        Ok(())
    }
}

// Merkle proof verification
fn verify_proof(proof: &[[u8; 32]], root: &[u8; 32], leaf: &[u8; 32]) -> bool {
    let mut computed_hash = *leaf;

    for proof_element in proof.iter() {
        if computed_hash <= *proof_element {
            computed_hash = keccak::hashv(&[&computed_hash, proof_element]).0;
        } else {
            computed_hash = keccak::hashv(&[proof_element, &computed_hash]).0;
        }
    }

    computed_hash == *root
}

// Accounts

#[derive(Accounts)]
pub struct InitializeCollection<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + CollectionConfig::INIT_SPACE,
        seeds = [b"config", authority.key().as_ref()],
        bump
    )]
    pub config: Account<'info, CollectionConfig>,

    #[account(
        init,
        payer = authority,
        mint::decimals = 0,
        mint::authority = config,
        mint::freeze_authority = config,
    )]
    pub collection_mint: Account<'info, Mint>,

    #[account(
        init,
        payer = authority,
        associated_token::mint = collection_mint,
        associated_token::authority = authority,
    )]
    pub collection_token: Account<'info, TokenAccount>,

    /// CHECK: Created by Metaplex
    #[account(mut)]
    pub collection_metadata: UncheckedAccount<'info>,

    /// CHECK: Created by Metaplex
    #[account(mut)]
    pub collection_master_edition: UncheckedAccount<'info>,

    /// CHECK: Treasury address
    pub treasury: UncheckedAccount<'info>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub metadata_program: Program<'info, Metadata>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct MintNft<'info> {
    #[account(
        mut,
        seeds = [b"config", config.authority.as_ref()],
        bump = config.bump,
    )]
    pub config: Account<'info, CollectionConfig>,

    #[account(
        init_if_needed,
        payer = minter,
        space = 8 + MintTracker::INIT_SPACE,
        seeds = [b"tracker", config.key().as_ref(), minter.key().as_ref()],
        bump
    )]
    pub mint_tracker: Account<'info, MintTracker>,

    #[account(
        init,
        payer = minter,
        mint::decimals = 0,
        mint::authority = config,
        mint::freeze_authority = config,
    )]
    pub nft_mint: Account<'info, Mint>,

    #[account(
        init,
        payer = minter,
        associated_token::mint = nft_mint,
        associated_token::authority = minter,
    )]
    pub nft_token: Account<'info, TokenAccount>,

    /// CHECK: Created by Metaplex
    #[account(mut)]
    pub nft_metadata: UncheckedAccount<'info>,

    /// CHECK: Created by Metaplex
    #[account(mut)]
    pub nft_master_edition: UncheckedAccount<'info>,

    /// CHECK: Verified in config
    #[account(
        mut,
        address = config.treasury,
    )]
    pub treasury: UncheckedAccount<'info>,

    #[account(mut)]
    pub minter: Signer<'info>,

    pub token_program: Program<'info, Token>,
    pub associated_token_program: Program<'info, AssociatedToken>,
    pub metadata_program: Program<'info, Metadata>,
    pub system_program: Program<'info, System>,
    pub rent: Sysvar<'info, Rent>,
}

#[derive(Accounts)]
pub struct UpdateConfig<'info> {
    #[account(
        mut,
        seeds = [b"config", config.authority.as_ref()],
        bump = config.bump,
        has_one = authority,
    )]
    pub config: Account<'info, CollectionConfig>,

    pub authority: Signer<'info>,
}

#[derive(Accounts)]
pub struct Withdraw<'info> {
    #[account(
        seeds = [b"config", config.authority.as_ref()],
        bump = config.bump,
        has_one = authority,
        has_one = treasury,
    )]
    pub config: Account<'info, CollectionConfig>,

    #[account(mut)]
    /// CHECK: Verified via has_one
    pub treasury: UncheckedAccount<'info>,

    #[account(mut)]
    pub authority: Signer<'info>,

    pub system_program: Program<'info, System>,
}

// State

#[account]
#[derive(InitSpace)]
pub struct CollectionConfig {
    pub authority: Pubkey,
    pub collection_mint: Pubkey,
    pub treasury: Pubkey,
    #[max_len(32)]
    pub name: String,
    #[max_len(10)]
    pub symbol: String,
    #[max_len(200)]
    pub base_uri: String,
    pub price: u64,
    pub whitelist_price: u64,
    pub max_supply: u32,
    pub items_minted: u32,
    pub start_time: i64,
    pub whitelist_end_time: i64,
    pub whitelist_root: [u8; 32],
    pub royalty_basis_points: u16,
    pub paused: bool,
    pub bump: u8,
}

#[account]
#[derive(InitSpace)]
pub struct MintTracker {
    pub count: u8,
}

// Events

#[event]
pub struct CollectionInitialized {
    pub config: Pubkey,
    pub collection_mint: Pubkey,
    pub max_supply: u32,
    pub price: u64,
}

#[event]
pub struct NftMinted {
    pub config: Pubkey,
    pub mint: Pubkey,
    pub minter: Pubkey,
    pub number: u32,
    pub is_whitelist: bool,
}

// Errors

#[error_code]
pub enum NftError {
    #[msg("Invalid max supply")]
    InvalidMaxSupply,

    #[msg("Invalid price")]
    InvalidPrice,

    #[msg("Invalid royalty (max 100%)")]
    InvalidRoyalty,

    #[msg("Minting is paused")]
    MintingPaused,

    #[msg("Collection is sold out")]
    SoldOut,

    #[msg("Minting has not started")]
    MintNotStarted,

    #[msg("Not on whitelist")]
    NotWhitelisted,

    #[msg("Wallet mint limit exceeded")]
    WalletLimitExceeded,
}

TypeScript Client

TypeScript
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { NftMinting } from "../target/types/nft_minting";
import * as token from "@solana/spl-token";
import {
  Metaplex,
  keypairIdentity,
  bundlrStorage,
} from "@metaplex-foundation/js";
import { MerkleTree } from "merkletreejs";
import { keccak256 } from "js-sha3";

export class NftMintingClient {
  private metaplex: Metaplex;

  constructor(
    private program: Program<NftMinting>,
    private provider: anchor.AnchorProvider,
  ) {
    this.metaplex = Metaplex.make(provider.connection)
      .use(keypairIdentity((provider.wallet as anchor.Wallet).payer))
      .use(bundlrStorage());
  }

  async initializeCollection(
    name: string,
    symbol: string,
    uri: string,
    price: anchor.BN,
    maxSupply: number,
    startTime: anchor.BN,
    whitelistPrice: anchor.BN,
    whitelistEndTime: anchor.BN,
    royaltyBasisPoints: number,
    treasury: anchor.web3.PublicKey,
  ): Promise<{
    config: anchor.web3.PublicKey;
    collectionMint: anchor.web3.PublicKey;
  }> {
    const authority = this.provider.wallet.publicKey;

    const [configPda] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("config"), authority.toBuffer()],
      this.program.programId,
    );

    const collectionMint = anchor.web3.Keypair.generate();

    const collectionToken = await token.getAssociatedTokenAddress(
      collectionMint.publicKey,
      authority,
    );

    const [collectionMetadata] = anchor.web3.PublicKey.findProgramAddressSync(
      [
        Buffer.from("metadata"),
        new anchor.web3.PublicKey(
          "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
        ).toBuffer(),
        collectionMint.publicKey.toBuffer(),
      ],
      new anchor.web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
    );

    const [collectionMasterEdition] =
      anchor.web3.PublicKey.findProgramAddressSync(
        [
          Buffer.from("metadata"),
          new anchor.web3.PublicKey(
            "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
          ).toBuffer(),
          collectionMint.publicKey.toBuffer(),
          Buffer.from("edition"),
        ],
        new anchor.web3.PublicKey(
          "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
        ),
      );

    await this.program.methods
      .initializeCollection(
        name,
        symbol,
        uri,
        price,
        maxSupply,
        startTime,
        whitelistPrice,
        whitelistEndTime,
        royaltyBasisPoints,
      )
      .accounts({
        config: configPda,
        collectionMint: collectionMint.publicKey,
        collectionToken,
        collectionMetadata,
        collectionMasterEdition,
        treasury,
        authority,
        tokenProgram: token.TOKEN_PROGRAM_ID,
        associatedTokenProgram: token.ASSOCIATED_TOKEN_PROGRAM_ID,
        metadataProgram: new anchor.web3.PublicKey(
          "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
        ),
        systemProgram: anchor.web3.SystemProgram.programId,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
      })
      .signers([collectionMint])
      .rpc();

    return {
      config: configPda,
      collectionMint: collectionMint.publicKey,
    };
  }

  async mintNft(
    config: anchor.web3.PublicKey,
    name: string,
    uri: string,
    whitelistProof?: Buffer[],
  ): Promise<anchor.web3.PublicKey> {
    const configData =
      await this.program.account.collectionConfig.fetch(config);
    const minter = this.provider.wallet.publicKey;

    const [trackerPda] = anchor.web3.PublicKey.findProgramAddressSync(
      [Buffer.from("tracker"), config.toBuffer(), minter.toBuffer()],
      this.program.programId,
    );

    const nftMint = anchor.web3.Keypair.generate();

    const nftToken = await token.getAssociatedTokenAddress(
      nftMint.publicKey,
      minter,
    );

    const [nftMetadata] = anchor.web3.PublicKey.findProgramAddressSync(
      [
        Buffer.from("metadata"),
        new anchor.web3.PublicKey(
          "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
        ).toBuffer(),
        nftMint.publicKey.toBuffer(),
      ],
      new anchor.web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
    );

    const [nftMasterEdition] = anchor.web3.PublicKey.findProgramAddressSync(
      [
        Buffer.from("metadata"),
        new anchor.web3.PublicKey(
          "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
        ).toBuffer(),
        nftMint.publicKey.toBuffer(),
        Buffer.from("edition"),
      ],
      new anchor.web3.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"),
    );

    const proof = whitelistProof?.map((p) => Array.from(p)) ?? null;

    await this.program.methods
      .mintNft(name, uri, proof)
      .accounts({
        config,
        mintTracker: trackerPda,
        nftMint: nftMint.publicKey,
        nftToken,
        nftMetadata,
        nftMasterEdition,
        treasury: configData.treasury,
        minter,
        tokenProgram: token.TOKEN_PROGRAM_ID,
        associatedTokenProgram: token.ASSOCIATED_TOKEN_PROGRAM_ID,
        metadataProgram: new anchor.web3.PublicKey(
          "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
        ),
        systemProgram: anchor.web3.SystemProgram.programId,
        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
      })
      .signers([nftMint])
      .rpc();

    return nftMint.publicKey;
  }
}

// Whitelist utilities
export function createMerkleTree(addresses: anchor.web3.PublicKey[]) {
  const leaves = addresses.map((addr) =>
    Buffer.from(keccak256(addr.toBuffer()), "hex"),
  );

  const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
  const root = tree.getRoot();

  return {
    tree,
    root: Array.from(root) as number[],
    getProof: (address: anchor.web3.PublicKey) => {
      const leaf = Buffer.from(keccak256(address.toBuffer()), "hex");
      return tree.getProof(leaf).map((p) => p.data);
    },
  };
}

Mint Phases

PhaseDurationPriceLimitAccess
WhitelistPre-launchWL Price2 per walletMerkle proof
PublicPost-WLFull Price5 per walletAnyone

Metadata Structure

JSON
{
  "name": "Collection Name #1",
  "symbol": "COLL",
  "description": "A unique NFT from the collection",
  "seller_fee_basis_points": 500,
  "image": "https://arweave.net/...",
  "external_url": "https://example.com",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Rarity", "value": "Legendary" }
  ],
  "properties": {
    "files": [{ "uri": "...", "type": "image/png" }],
    "category": "image",
    "creators": [{ "address": "...", "share": 100 }]
  }
}

Next: Data Indexing - Indexing and querying on-chain data.