w3-kitdocs

mint-nft

Mint a new NFT on EVM (ERC-721) or Solana (SPL Token with 0 decimals and Metaplex metadata).

evmsolana

Dependencies

viem@solana/web3.js@metaplex-foundation/js
mint-nft/evm.tsx
"use client";

import { useAccount, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { useState } from "react";

// ★ Minimal ERC-721 ABI — safeMint requires the contract to be pre-deployed
// In production, deploy an OpenZeppelin ERC721 contract and use its address
const erc721MintAbi = [
  {
    name: "safeMint",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "to", type: "address" },
      { name: "tokenId", type: "uint256" },
      { name: "uri", type: "string" },
    ],
    outputs: [],
  },
  {
    name: "totalSupply",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ name: "", type: "uint256" }],
  },
] as const;

export function MintNFT() {
  const { address } = useAccount();
  const [contractAddress, setContractAddress] = useState("");
  const [recipient, setRecipient] = useState("");
  const [tokenId, setTokenId] = useState("");
  const [tokenUri, setTokenUri] = useState("");

  const { writeContract, data: hash, isPending, error } = useWriteContract();
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });

  const handleMint = () => {
    writeContract({
      address: contractAddress as `0x${string}`,
      abi: erc721MintAbi,
      functionName: "safeMint",
      args: [
        (recipient || address) as `0x${string}`,
        BigInt(tokenId),
        tokenUri,
      ],
    });
  };

  return (
    <div>
      <h2>Mint NFT (ERC-721)</h2>
      <input
        value={contractAddress}
        onChange={(e) => setContractAddress(e.target.value)}
        placeholder="NFT contract address (0x...)"
      />
      <input
        value={recipient}
        onChange={(e) => setRecipient(e.target.value)}
        placeholder={`Recipient (default: ${address ?? "your wallet"})`}
      />
      <input
        value={tokenId}
        onChange={(e) => setTokenId(e.target.value)}
        placeholder="Token ID (e.g., 1)"
      />
      <input
        value={tokenUri}
        onChange={(e) => setTokenUri(e.target.value)}
        placeholder="Token URI (ipfs://... or https://...)"
      />
      <button onClick={handleMint} disabled={isPending || !contractAddress || !tokenId}>
        {isPending ? "Minting..." : "Mint NFT"}
      </button>
      {isConfirming && <p>Waiting for confirmation...</p>}
      {isSuccess && <p>NFT minted! Tx: {hash}</p>}
      {error && <p>Error: {error.message}</p>}
    </div>
  );
}

Mint NFT — Learn

What is an NFT?

An NFT (Non-Fungible Token) is a token where each unit is unique and not interchangeable. A fungible token (like ETH or USDC) is interchangeable — one ETH equals any other ETH. An NFT is different: token #1 is not the same as token #2, even in the same collection.

The "non-fungible" property is enforced by the token standard:

  • EVM: ERC-721 gives each token a unique tokenId and tracks ownership with ownerOf(tokenId)
  • Solana: A mint account with 0 decimals and a max supply of 1 — physically impossible to have more than one

How ERC-721 works

// Core ERC-721 storage
mapping(uint256 => address) private _owners;       // tokenId → owner
mapping(address => uint256) private _balances;      // owner → count
mapping(uint256 => address) private _tokenApprovals; // tokenId → approved spender

function safeMint(address to, uint256 tokenId, string memory uri) external onlyOwner {
    _mint(to, tokenId);
    _setTokenURI(tokenId, uri); // stores the metadata URI
}

function ownerOf(uint256 tokenId) public view returns (address) {
    return _owners[tokenId]; // O(1) lookup
}

The ERC-721 standard (EIP-721) defines these required functions:

  • balanceOf(address) — how many NFTs does this address own?
  • ownerOf(tokenId) — who owns this specific token?
  • transferFrom(from, to, tokenId) — transfer ownership
  • approve(to, tokenId) — approve someone to transfer a specific token
  • tokenURI(tokenId) — get the metadata URI for a token

How Solana NFTs work

On Solana, an NFT is defined by three constraints on an SPL mint account:

  1. 0 decimals — can't have 0.5 of the token
  2. Max supply of 1 — mint authority is used exactly once, then (optionally) revoked
  3. Metaplex metadata — a separate Program Derived Address account stores name, symbol, URI
NFT identity: mint account (PublicKey)
     ↓
Metaplex Metadata PDA: name, symbol, uri, creators, royalties
     ↓
Token account (ATA): holds the 1 token, owned by the NFT holder's wallet

The Metaplex Token Metadata Program is the de facto standard for Solana NFT metadata.

Metadata standards

Both EVM and Solana store NFT metadata off-chain (usually IPFS or Arweave) and reference it with a URI.

ERC-721 Metadata JSON

{
  "name": "My NFT #1",
  "description": "A description of this NFT",
  "image": "ipfs://QmXxx.../image.png",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Eyes", "value": "Laser" }
  ]
}

Metaplex JSON Standard

{
  "name": "My NFT #1",
  "symbol": "MNFT",
  "description": "A description of this NFT",
  "seller_fee_basis_points": 500,
  "image": "https://arweave.net/xxx/image.png",
  "attributes": [
    { "trait_type": "Background", "value": "Blue" },
    { "trait_type": "Eyes", "value": "Laser" }
  ],
  "properties": {
    "files": [
      { "uri": "https://arweave.net/xxx/image.png", "type": "image/png" }
    ],
    "creators": [{ "address": "YourWallet...", "share": 100 }]
  }
}

IPFS vs Arweave

IPFSArweave
CostFree to pin (via Pinata/NFT.storage); pay for persistenceOne-time fee (~$0.01/MB)
PermanenceData can disappear if unpinnedPermanent by design
URI formatipfs://QmHash...ar://TxId... or https://arweave.net/TxId
SpeedVariable gateway speedFast via arweave.net

For production NFT collections, Arweave is preferred because the data is permanent. IPFS data can disappear if nobody pins it.

The minting flow

EVM full flow

  1. Deploy an ERC-721 contract (once per collection)
  2. Call safeMint(to, tokenId, tokenURI) for each NFT
  3. The contract stores _owners[tokenId] = to
  4. Wallets and marketplaces read tokenURI(tokenId) to display the NFT

Solana full flow

  1. Generate a new keypair for each NFT mint
  2. Create the mint account (SystemProgram.createAccount)
  3. Initialize as SPL mint with 0 decimals (createInitializeMintInstruction)
  4. Create the recipient's Associated Token Account
  5. Mint exactly 1 token (createMintToInstruction)
  6. (Optional but recommended) Attach Metaplex metadata via createCreateMetadataAccountV3Instruction
  7. (Optional) Revoke mint authority — proves no more can ever be minted

Revoking mint authority

After minting your collection, you can make it immutable by revoking the mint authority:

// Solana — after minting all tokens
await setAuthority(
  connection,
  payer,
  mintPubkey,
  currentAuthority,
  AuthorityType.MintTokens,
  null, // ★ null = revoke
);

On EVM, you renounce ownership of the contract (or remove the minter role), preventing any future mints.