Skip to main content
Follow the NFT template file by file: the mint rules, the artwork the contract draws itself, and the gallery that finds your tokens without event logs.
Ready. This template is published in uzolabs/templates and has been deployed and verified on testnet. It’s a learning template and hasn’t been audited. Use test funds only.

Project layout

Project layout
The deploy, verify and toolchain setup work the same way as in the token template. See Token walkthrough.

The contract

contracts/UzoNFT.sol combines four OpenZeppelin contracts: The constructor takes (name, symbol, mintPrice, maxSupply, owner). maxSupply is immutable, so it can’t change after deploy. Both functions that move value are nonReentrant and update state before any external call. withdraw uses a low-level call, so an owner that’s a contract wallet still gets paid.

The on-chain artwork

tokenURI(id) builds the metadata each time it’s called:
  1. Hash the token ID to pick a hue from 0 to 359.
  2. Draw a 400 by 400 SVG: a diagonal gradient between a light and a dark shade of that hue, with the ID in large white monospace text.
  3. Wrap the SVG and the JSON in Base64 and return a data:application/json;base64,... URI.
Decoded tokenURI
Wallets, explorers and marketplaces read this format. The contract stores nothing per token beyond its owner, so minting stays cheap.

Tests

test/UzoNFT.t.sol holds 28 tests. They cover payment and supply rules, owner functions, and decode the JSON and SVG that tokenURI returns.
Terminal

Minting from the command line

Terminal
Output

The web app

frontend/src/App.tsx reads the name, price, supply, max supply and owner in one batched call and polls every 10 seconds. The gallery then reads: All three steps go through Multicall3, so a wallet with many tokens still loads in a few requests. The gallery shows images with <img>, which never runs scripts inside an SVG, and only accepts data:image/svg+xml images. Errors such as WrongPayment and SoldOut appear in plain words.

Optional: pinning to IPFS

The contract doesn’t need IPFS. If you change tokenURI to point at IPFS, for example for photos too large to store on chain, scripts/upload-metadata.ts is a starting point. Set PINATA_JWT in .env with a key from Pinata, then run:
Terminal
It pins token 1’s SVG, then its JSON pointing at the pinned image, and prints both ipfs:// links. Without PINATA_JWT it does nothing.

Next steps

Customize

Change the art, price rules and royalties.

Read contract state

Batched reads with wagmi.
Last modified on October 3, 2026