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 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:
- Hash the token ID to pick a hue from 0 to 359.
- 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.
- Wrap the SVG and the JSON in Base64 and return a
data:application/json;base64,...URI.
Decoded tokenURI
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 changetokenURI 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
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.