ICE 6 – ERC‑721 NFT (OpenZeppelin + Remix)

Goal: build a minimal ERC‑721 NFT, attach a tokenURI with metadata (image, name, description), deploy with MetaMask → Ganache, then preview it with a tiny HTML/JS viewer.

0) Prereqs

1) Create the NFT contract

In Remix, make MyNFT.sol and paste:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/access/Ownable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/utils/Counters.sol";

contract MyNFT is ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor(string memory name_, string memory symbol_)
        ERC721(name_, symbol_)
        Ownable(msg.sender)
    {}

    function mintTo(address to, string memory uri) public onlyOwner returns (uint256) {
        _tokenIds.increment();
        uint256 newId = _tokenIds.current();
        _mint(to, newId);
        _setTokenURI(newId, uri);
        return newId;
    }
}

If the GitHub imports fail, try another stable OZ version (e.g., v5.0.x) or pin libraries via Remix’s Libraries plugin.

2) Compile

  1. Solidity Compiler → select 0.8.20 (or compatible 0.8.x).
  2. Click Compile MyNFT.sol and wait for the green check.

3) Prepare metadata (tokenURI)

An NFT’s tokenURI points to a JSON like:

{
  "name": "FIN451 Badge #1",
  "description": "Class NFT for ICE 6",
  "image": "https://upload.wikimedia.org/wikipedia/commons/3/36/Meta-Logo.png",
  "attributes": [
    {"trait_type": "Course", "value": "FIN451"},
    {"trait_type": "ICE", "value": "6"}
  ]
}
  1. Create a file metadata1.json with your own name, description, and an image URL (any public image URL works for class).
  2. Host it anywhere reachable by URL (e.g., on your class site). Example URL: https://yourdomain/fin451_25f/nft/metadata1.json

Optional: use IPFS (Pinata/web3.storage) and set tokenURI to ipfs://...CID.... For now, a plain HTTPS URL is fine.

4) Deploy (MetaMask → Ganache)

  1. Deploy & Run in Remix → Environment: Injected Provider – MetaMask (Localhost/Ganache).
  2. Contract: MyNFT.
  3. Constructor inputs:
    • name_: e.g., FIN451 NFT
    • symbol_: e.g., FINNFT
  4. Click Deploy → confirm in MetaMask.

Gas estimation failed? Make sure your selected MetaMask account has Ganache ETH and that Injected Provider shows Localhost.

5) Mint an NFT

  1. Under Deployed Contracts, expand your MyNFT instance.
  2. Paste your own wallet address (from MetaMask) into mintTo(to, uri).
  3. For uri, paste your hosted metadata URL (e.g., https://yourdomain/.../metadata1.json).
  4. Click transact → confirm in MetaMask. It returns a tokenId (e.g., 1).
  5. Call ownerOf(1) → should be your address. Call tokenURI(1) → should echo the URL.

6) (Optional) Tiny NFT Viewer (HTML/JS)

Save as nft_viewer.html. Paste your contract address and ABI (Remix → Compilation Details → ABI). Enter a tokenId to fetch and render its image.

<!DOCTYPE html>
<html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>NFT Viewer</title></head>
<body>
  <button id="connect">Connect MetaMask</button>
  <div>Contract: <input id="addr" value="PASTE_CONTRACT_ADDRESS" style="width:360px"/></div>
  <textarea id="abi" rows="6" style="width:100%">PASTE_ABI_JSON_ARRAY</textarea>
  <div>Token ID: <input id="tid" type="number" value="1" style="width:120px"/> <button id="load">Load</button></div>
  <div id="out"></div>
  <script src="https://cdn.jsdelivr.net/npm/ethers@6.13.2/dist/ethers.min.js"></script>
  <script>
    let provider;
    document.getElementById('connect').onclick = async () => {
      if(!window.ethereum) return alert('MetaMask not found');
      provider = new ethers.BrowserProvider(window.ethereum);
      await provider.send('eth_requestAccounts', []);
    };
    document.getElementById('load').onclick = async () => {
      const addr = document.getElementById('addr').value.trim();
      const abi = JSON.parse(document.getElementById('abi').value.trim());
      const tid = document.getElementById('tid').value;
      const c = new ethers.Contract(addr, abi, provider || new ethers.BrowserProvider(window.ethereum));
      const uri = await c.tokenURI(tid);
      const res = await fetch(uri);
      const meta = await res.json();
      document.getElementById('out').innerHTML = `
        

${meta.name || ''}

${meta.description || ''}

nft image
${JSON.stringify(meta, null, 2)}
`; }; </script> </body></html>

Some NFTs use ipfs:// image links. For a quick demo, keep images on plain HTTPS while learning.

Submission

  • Screenshot: Deployed MyNFT contract & address (Ganache).
  • Screenshot: Successful mintTo(to, uri) for tokenId 1 and tokenURI(1).
  • Screenshot: nft_viewer.html rendering the image and showing JSON.
  • Short reflection: How metadata works; storage off‑chain vs on‑chain; why events help UIs.

Troubleshooting

  • Import URL failed: Try another OZ version (v5.0.x) or use Remix Libraries.
  • Gas estimation failed: Fund account; confirm Injected Provider shows Localhost.
  • Viewer can’t fetch: Ensure tokenURI is a reachable HTTPS URL returning JSON with an image field.
  • Image not loading: Try a different public image URL or host it on your class site.

Next: coming soon.