Tutorial

Fully On-Chain SVG NFTs — Part 3: Minting Website

How to create a minting website for NFT projects.

Written By

Eto Vass

Published On


Fully On-Chain SVG NFTs — Part 3: Minting Website

In our previous two tutorials (part 1 and part 2), I covered creating an SVG rendering contract and an ERC-721 mint-able contract with OpenZeppelin libraries and deploying the contracts on the blockchain.

In this third tutorial, I will present how to create a minting website for your on-chain (and not only) projects.

Intro

Ethereum network operates as a set of nodes that communicate with each other. To communicate with the Ethereum network, your app must make RPC calls to some nodes. Fortunately, you don’t need to run your own nodes; there are node providers like Alchemy, Infura, Thirdweb, Quicknode, and many others that simplify this process. There are also free options (Cloudflare, etc) that can used for testing, but they are not recommended for production since they are rate-limited. A very simplified diagram of what we will cover in this tutorial:

mint1

We will create a Dapp that allows users to interact with your ERC-721 contract — like mint, get contact information, show tokens owned by wallet, etc.

Mint.fun

Before I continue, sometimes your on-chain collection may be simple enough not to need a full-blown minting website. Marketplaces are suitable for listing your collection, and you can use sites like Mint.fun, which allows you to quickly build a simple minting web page for your ERC-721 contracts without coding skills.

Here is an example with Alien Runes, my first on-chain collection: https://mint.fun/ethereum/0xb44298c4ef2f474a25A570CE23EcE36Ecfcd45D3

If you still need a minting website — please continue reading…

Technical stack

For this tutorial, we will use the following frameworks and libraries:

  • React — one of the most commonly used libraries for web apps.
  • Next.js — opinionated (IMO — in a good way) and very easy-to-use framework that allows quickly - building React apps (that support server-side rendering out of the box).
  • Tailwind —a very easy-to-use CSS framework
  • Wagmi — a library for interacting with Ethereum
  • RainbowKit — an easy and user-friendly library to connect wallets to your web app

Setup

As with the previous tutorial, I’ll guide you through each of the steps. If you want to go directly to the code — you can find it in the tutorial Github repo.

Create directory tutorial-3-minting-website, and install React and Next.js, create Next.js app. Select Yes for everything or defaults. Name your project nft-mint.

mint2

This will install React, Next.js, and Tailwind and will create Next.js app. It will create a folder called nft-mint where all of the code will be. Go to this folder and run npm run dev

mint3

If everything is OK, you will be able to see the default Next.js page by going to http://localhost:3000.

Next:

  1. Edit src/app/globals.css and select your default foreground/text and background colors in :root

mint4

  1. Edit src/app/layout.tsx — metadata, to give your site title and description (that will be visible in browsers)

mint5

  1. Edit src/app/page.tsx to show a minimal site with one line of text:
1export default function Home() {
2 return (
3 <main className="mt-10 flex flex-col gap-8 items-center justify-center ">
4 <h1>SVG NFT Minting Website</h1>
5 </main>
6 );
7}

If everything is correct, once you save the files, you will see the updated home page:

mint6

Wagmi & RainbowKit

Now, we are ready to start creating code to interact with the contract. First, we need to install Wagmi and RainbowKit in the nft-mint directory.

1npm install --save @rainbow-me/rainbowkit wagmi viem@2.x @tanstack/react-query

This will install Wagmi, RainbowKit and also Viem, and Tanstack, which are needed peer dependencies.

Create file src/wagmi.ts with the following content:

1import '@rainbow-me/rainbowkit/styles.css';
2
3import { fallback, http } from 'wagmi';
4import { sepolia } from 'wagmi/chains';
5import { web3config } from './dapp.config';
6import { darkTheme, getDefaultConfig } from '@rainbow-me/rainbowkit';
7
8export const wagmiConfig = getDefaultConfig({
9 appName: 'My NFT Minting App',
10 projectId: 'YOUR_WALLET_CONNECT_PROJECT_ID',
11 chains: [sepolia],
12 transports: {
13 [sepolia.id]: fallback([
14 http(`https://eth-sepolia.g.alchemy.com/v2/${web3config.alchemyApiKey}`),
15 ])
16
17 },
18 ssr: true
19});
20
21export const myRainbowTheme = darkTheme({
22 accentColor: '#C2410C',
23 accentColorForeground: 'white',
24 borderRadius: 'large'
25});

This file contains the configuration for Wagmi and Rainbow. In wagmiConfig, we define the supported chains (in our case, Ethereum Sepolia) and the nodes that will be used to access them. It is an excellent practice to have more than 1 in case some are not responding, or you hit some limits if your collection is very successful. Check https://www.rainbowkit.com/docs/installation and https://wagmi.sh/react/getting-started for more details on how to use other chains. Localhost is also supported for locally running test nodes, which is very helpful if you haven’t finalized the contract yet. If your app relies on WalletConnect, you need to obtain a projectId from WalletConnect Cloud (it is free).

In myRainbowTheme, we customize some of the visuals for RainbowKit—in our case, to match the visuals of the minting site. Check here for the full details on how to customize it here.

Create file src/app/providers.tsx with the following content:

1'use client';
2
3import React from 'react';
4import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5import { WagmiProvider } from 'wagmi';
6import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
7
8import { wagmiConfig, myRainbowTheme } from '../wagmi';
9
10const queryClient = new QueryClient();
11
12export function Providers({ children }: { children: React.ReactNode }) {
13 return (
14 <WagmiProvider config={wagmiConfig}>
15 <QueryClientProvider client={queryClient}>
16 <RainbowKitProvider theme={myRainbowTheme}>{children}</RainbowKitProvider>
17 </QueryClientProvider>
18 </WagmiProvider>
19 );
20}

This file is needed to inject Wagmi and RainbowKit to our React app.

And edit src/layout.tsx to wrap children with Providers

mint7

Once this is done, our React app will have access to Rainbow and Wagmi, and we can take the next step.

Connecting your wallet

Before you can mint, you shall connect your wallet. This happens with just several lines of code, thanks to Wagmi and RainbowKit.

Modify src/app/page.tsx and add ‘use client’ to the top, this will make the page client side. Also, import ConnectButton from RainbowKit and add it to the page:

1'use client'
2
3import { ConnectButton } from "@rainbow-me/rainbowkit";
4import { useAccount } from "wagmi";
5
6export default function Home() {
7 const {address, isConnected} = useAccount();
8
9 return (
10 <main className="mt-10 flex flex-col gap-8 items-center justify-center ">
11 <h1>SVG NFT Minting Website</h1>
12
13 <ConnectButton label="Connect Wallet to Mint" />
14 </main>
15 );
16}

Now the page will look like:

mint8

and if you click the button and connect your wallet — it will be show on the page:

mint9

Wagmi CLI

Wagmi CLI is a great addition to Wagmi that allows you to achieve full type safety when calling contracts from Typescript code. You can install it with:

1npm install --save-dev @wagmi/cli

Then you can configure it by adding src/wagmi.config.ts with the following content

1import { defineConfig } from '@wagmi/cli'
2import { actions, foundry, react } from '@wagmi/cli/plugins'
3
4export default defineConfig({
5 out: 'src/contracts-generated.ts',
6 contracts: [],
7 plugins: [
8 foundry({
9 artifacts: "../../tutorial-2-NFT-contract",
10 include: [
11 "NFTManager.sol/**"
12 ]
13 }),
14 ],
15})

This instructs Wagmi CLI to look at the ABI definition of NFTManager.sol and generate a file src/contracts-generated.ts that Wagmi can use for type safety. You can also generate Wagmi actions and react hooks from the contract, get contact information from Etherscan, integrate with Foundry and Hardhat, and many others. Check Wagmi CLI plugin documentation for more details. In this tutorial, I use Foundry plugin and tell Wagmi CLI where to look for contract data (artifacts attribute) as well, the contract name we will interact with (include attribute).

Also add the following in package.json

1"scripts": {
2 ...
3 "wagmi:generate": "cd ../../tutorial-2-NFT-contract && forge clean && forge build && cd ../tutorial-3-minting-website/nft-mint && wagmi generate"
4 ...

Now if you run:

1npm run wagmi:generate

then Foundry contracts will be compiled, src/contracts-generated.ts will be created, and you can use it in Wagmi.

Putting things together

We now have everything needed to create mint functionality.

Create a file src/dapp.config.ts with the following content:

1export const web3config = {
2 alchemyApiKey: '<YOUR_ALCHEMY_API_KEY>',
3 contractAddress: '0x331dbe16931a959055b73ce6cd2a1006ce79972b',
4};

Put your Alchemy API key in alchemyApiKey. In GitHub, I will leave one of my keys restricted only to Sepolia so you can quickly get this up and running. But I cannot guarantee that this key will work forever. If it gets abused, I will disable it.

Note: putting API keys in your code is not a best practice, and it is not secure. It will be best to put them in environment variables and use them as such in the code. It is outside this tutorial's scope to dive deep into how to pass env variables to the Next.js application. This is very well documented in Next.js documentation here: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables

contractAdddress is the address of the deployed contract in Sepolia, which will be used for minting. By default, I use the same contract that was deployed to Sepolia in Part 2, but at some point, it may get minted out. The best thing to do is to replace it with your own contract.

Next we need to modify src/app/page.tsx to implement minting functionality.

Import all dependencies that you will need:

1'use client'
2
3import { ConnectButton } from "@rainbow-me/rainbowkit";
4import { useAccount, useReadContract } from "wagmi";
5import { web3config } from "../dapp.config";
6import { Address } from "viem";
7import { nftManagerAbi } from "../contracts-generated";
8import { useState } from "react";
9import { formatEther } from "viem";
10import { writeContract, waitForTransactionReceipt, readContract } from "wagmi/actions";
11import { wagmiConfig } from "@/wagmi";

Continue with adding MintNFT component after ConnectButton component

1<ConnectButton label="Connect Wallet to Mint" />
2
3{isConnected && <MintNFT />}

Define MintState enum after the imports, this enum will define where we are in the mint process:

1enum MintState {
2 IDLE,
3 WAIT_TO_CONFIRM_TRANSACTION,
4 WAITING_FOR_TRANSACTION_RECEIPT,
5 TRANSACTION_CONFIRMED,
6 TRANSACTION_ERROR
7}

Then, we need to implement MintNFT component, starting with:

1function MintNFT() {
2 const {address} = useAccount();
3 const [mintState, setMintState] = useState(MintState.IDLE);
4 const [error, setError] = useState('');
5 const [mintedId, setMintedId] = useState('');
6 const [mintedTokenURI, setMintedTokenURI] = useState('');

There are the state variables that will be used in MintNFT component.

  • address is the connected wallet’s address, NFTs will be minted to this address.
  • mintState is where we are in the minting process. IDLE means it hasn’t started yet.
  • error is used in case mint fails
  • mintedId is the ID of the token that was recently minted
  • mintedTokeURI is the URI of the token that was recently minted

Then add:

1const {data: price, isLoading: isLoadingPrice, isError: isErrorPrice, error: errorPrice} = useReadContract({
2 address: web3config.contractAddress as Address,
3 abi: nftManagerAbi,
4 functionName: 'PRICE'
5 });

This will read the mint price from the deployed contract. It uses useReadContract hook from Wagmi. The ‘PRICE’ is a public constant defined in tutorial-2-NFT-contract/src/NFTManager.sol:

mint10

This is helpful to avoid hardcoding mint price in your Dapp, but instead — to retrieve it from the contract at mint time.

Add function mint. This is where the mint magic happens:

1const mint = async () => {
2 setMintState(MintState.WAIT_TO_CONFIRM_TRANSACTION);
3
4 try {
5 const mintPriceInWei = price;
6
7 // 1 - call safeMint
8 let tx = await writeContract(wagmiConfig, {
9 address: web3config.contractAddress as Address,
10 abi: nftManagerAbi,
11 functionName: "safeMint",
12 args: [address as Address],
13 value: mintPriceInWei,
14 });
15
16 setMintState(MintState.WAITING_FOR_TRANSACTION_RECEIPT);
17
18 // 2 - wait for transaction receipt
19 let receipt = await waitForTransactionReceipt(wagmiConfig, {
20 hash: tx
21 });
22
23 // 3 - get minted ID
24 const mintedID = BigInt(receipt?.logs[0]?.topics[3] as string);
25 setMintedId(mintedID.toString());
26
27 // 4 - get token URI
28 const tokenURI = await readContract(wagmiConfig, {
29 address: web3config.contractAddress as Address,
30 abi: nftManagerAbi,
31 functionName: "tokenURI",
32 args: [mintedID]
33 });
34
35 setMintedTokenURI(tokenURI);
36
37 setMintState(MintState.TRANSACTION_CONFIRMED);
38 } catch (error) {
39 setMintState(MintState.TRANSACTION_ERROR);
40 setError(error?.toString() ?? 'Unknown error');
41 }
42 }

There are 4 major steps in this function

Step 1: Calls safeMint()

mint11

This calls the safeMint() from NTFManager.sol and passes the address to which the new NFT will be minted, as well — the amount (in value attribute) that will be paid for the mint.

mint12

Step 2: Wait for the transaction receipt (transaction to be completed):

mint13

Step 3: Gets minted ID from transaction log

mint14

This is extracting the minted ID from the transaction log, as viewed in Etherscan:

mint15

Step 4: Calls tokenURI() from the contract to get information for the newly minted token.

mint16

This calls the following function from NFTManager.sol

mint17

Add two more functions:

1function formatTokenURI(tokenURI: string) {
2 const json = JSON.parse(atob(tokenURI.split(',')[1]));
3 delete json.image;
4 return JSON.stringify(json, null, 2);
5 }
6
7 function getImageURL(mintedTokenURI: string): string | undefined {
8 const json = JSON.parse(atob(mintedTokenURI.split(',')[1]));
9 return json.image;
10 }

The first will base64 decode the JSON returned by tokenURI(), which will later be visualized on successful mint. image attribute is deleted, since it is extracted separately and is not necessary to be show, it may also be very big.

The second function will base64 decode the JSON returned by tokenURI(), and will return an image that can be used in <img> tag

Add return statement for the component and all visualization logic:

1return (
2 <div>
3 {isLoadingPrice && <p>Loading price...</p>}
4 {isErrorPrice && <span>Error: {errorPrice.message}</span>}
5 {price &&
6 <div className="flex flex-col items-center">
7 <p>Minting from contract address: {web3config.contractAddress}</p>
8 <div className="flex flex-col items-center mt-4 font-bold">
9 {(mintState === MintState.IDLE || mintState === MintState.TRANSACTION_CONFIRMED) &&
10 <button onClick={mint} className="bg-[#C2410C] text-white px-4 py-2 rounded-lg">Mint for {formatEther(price)} ETH</button>
11 }
12 {mintState === MintState.WAIT_TO_CONFIRM_TRANSACTION &&
13 <p>Please confirm the transaction from your wallet...</p>
14 }
15 {mintState === MintState.WAITING_FOR_TRANSACTION_RECEIPT &&
16 <p>Waiting for transaction receipt...</p>
17 }
18 {mintState === MintState.TRANSACTION_CONFIRMED &&
19 <div className="flex flex-col items-center justify-center mt-4 font-bold text-green-500">
20 <p>Transaction confirmed!</p>
21 <p>Minted ID: {mintedId}</p>
22 <img className="m-4 w-[256px] h-[256px] border-2 rounded-lg border-gray-500 p-4" src={getImageURL(mintedTokenURI)} alt="NFT" />
23 <pre className="m-4 text-sm whitespace-break-spaces break-all border-2 rounded-lg border-gray-500 p-4" >Token URI: <br/>{formatTokenURI(mintedTokenURI)}</pre>
24 </div>
25 }
26 {mintState === MintState.TRANSACTION_ERROR &&
27 <div>
28 <p className="text-red-500">Transaction failed!</p>
29 <pre className="text-red-500">{error}</pre>
30 </div>
31 }
32 </div>
33 </div>
34 }
35 </div>
36 );

This is where the visualization happened. Based on the mintState, different things will be show on in the app.

Testing the mint page

After the previous step, we have everything needed to mint. We can test it now.

If you connect your wallet, you will see:

mint18

Clicking on Mint will ask you to confirm the transaction in your wallet. Once you do this, the mint process will start. On success (it takes a few seconds), you will see the newly minted token and its traits:

mint19

Congratulations, you implemented the mint functionality for your contracts.

List tokens owned by a wallet

The contract in part 2 that we use here uses the ERC721Enumerable extension (a good description of the extension can be found here: https://medium.com/@juanxaviervalverde/erc721enumerable-extension-what-how-and-why-8ba3532ea195) which allows us to enumerate all tokens that are owned by a particular wallet. We can use this to show tokens owned by a specific wallet address.

Add the following to src/app/page.tsx:

1function ShowMyButton() {
2 return (
3 <Link href='/my-tokens'>
4 <button className="bg-[#C2410C] font-bold text-white px-4 py-2 rounded-lg">View My Tokens</button>
5 </Link>
6 );
7}

and also

1{isConnected && <ShowMyButton />}

after <ConnectButton … >

This will add a new button View My Tokens that will redirect to /my-tokens page to show the tokens owned by an address.

mint20

We need to implement /my-tokens page now. Since we are using the App router for Next.js, we need to add a directory called my-tokens in src/app. Create a file called page.tsx in this new directory. This new file will be responsible for rendering /my-tokens page.

Put the following content in new pages.tsx

1'use client'
2
3import { nftManagerAbi } from "@/contracts-generated";
4import { web3config } from "@/dapp.config";
5import { ConnectButton } from "@rainbow-me/rainbowkit";
6import { Address } from "viem";
7import { useAccount, useReadContract } from "wagmi";
8
9function getImageURL(mintedTokenURI: string): string | undefined {
10 const json = JSON.parse(atob(mintedTokenURI.split(',')[1]));
11 return json.image;
12}
13
14function GetTokenURI({address, tokenId }: { address: string, tokenId: number }) {
15 const {data: tokenURI, isLoading: isLoadingTokenURI, isError: isErrorTokenURI, error: errorTokenURI} = useReadContract({
16 address: web3config.contractAddress as Address,
17 abi: nftManagerAbi,
18 functionName: "tokenURI",
19 args: [BigInt(tokenId)]
20 })
21
22 if (isLoadingTokenURI) return <div>Loading...</div>;
23 if (isErrorTokenURI) return <div>Error: {errorTokenURI?.message}</div>;
24
25 return <div className="w-[128px] h-[128px] flex flex-col items-center p-4 border-2 border-gray-300 rounded-lg"><img src={getImageURL(tokenURI as string)} /></div>;
26}
27
28function GetAndShowToken({address, tokenNumber }: { address: string, tokenNumber: number }) {
29 const {data: tokenId, isLoading: isLoadingToken, isError: isErrorToken, error: errorToken} = useReadContract({
30 address: web3config.contractAddress as Address,
31 abi: nftManagerAbi,
32 functionName: "tokenOfOwnerByIndex",
33 args: [address as Address, BigInt(tokenNumber)]
34 })
35
36 if (isLoadingToken) return <div>Loading...</div>;
37 if (isErrorToken) return <div>Error: {errorToken?.message}</div>;
38
39 return (
40 <div>
41 <GetTokenURI address={address} tokenId={Number(tokenId)} />
42 </div>
43 )
44}
45
46function VisualizeMyTokens({address, balance}: {address: string, balance: number}) {
47 let tokens = [];
48
49 for (let i = 0; i < balance; i++) {
50 tokens.push(<GetAndShowToken address={address} tokenNumber={i} />);
51 }
52
53 return (
54 <div className="flex flex-row flex-wrap items-center justify-center gap-4">
55 {tokens}
56 </div>
57 );
58}
59
60function ShowMyTokens({address}: {address: string}) {
61 const {data: tokens, isLoading: isLoadingTokens, isError: isErrorTokens, error: errorTokens} = useReadContract({
62 address: web3config.contractAddress as Address,
63 abi: nftManagerAbi,
64 functionName: "balanceOf",
65 args: [address as Address]
66 })
67
68 if (isLoadingTokens) return <div>Loading...</div>;
69 if (isErrorTokens) return <div>Error: {errorTokens?.message}</div>;
70
71 return (
72 <div className="flex flex-col items-center justify-center gap-4">
73 <h1>My Tokens: {tokens?.toString()}</h1>
74 <VisualizeMyTokens address={address} balance={Number(tokens)} />
75 </div>
76 );
77}
78
79export default function MyTokens() {
80 const {isConnected, address} = useAccount();
81
82 return (
83 <main className="mt-10 flex flex-col gap-8 items-center justify-center ">
84 <ConnectButton label="Connect Wallet to view your tokens" />
85
86 {isConnected && <ShowMyTokens address={address as string} />}
87 </main>
88 );
89}

The components in the file above are responsible for:

  • MyTokens — to make sure that we have a connected address.
  • ShowMyTokens — to extract the number of tokens an address owns, this is done by calling balanceOf() of the contract.
  • VisualizeMyTokens — from the balance, will create a new component (GetAndShowToken) for each owned token.
  • GetAndShowToken — extracts the tokenId for the current token, by calling tokenOfOwnerByIndex.
  • GetTokenURI — calls the tokenURI() from the conract and visualizes the image attribute.

The code above could potentially be simplified using useEffect(), useState() and async Wagmi calls, but for the purpose of the tutorial, I wanted to show how this can be done with Wagmi hooks, which requires a little bit more code, but is straightforward to implement and comprehense.

If everything is OK, after clicking on View My Tokens, you will see the tokens owned by the current address.

mint21

Final words Congratulations, you learned how to create NFT minting website. This tutorial gave you the basics that you can extend for your minting website. Do not hesitate to contact me at https://x.com/EtoVass if you have questions.

I’m also looking forward to seeing your fully on-chain collections released.

That is all, hope you enjoyed the tutorial!

mint22

Thank you for reading. If you like this tutorial, please support the author by minting one or more Alien Runes. Alien Runes is a fully on-chain NFT collection that generates SVG content from Solidity code and has complex algorithms to produce infinite unique runes. Available here: https://alienrunes.xyz/

Links

Git Repo for this tutorial:

More On-Chain Projects

Panopticon

created by: teto
Nouns

Nouns

created by: Nouns
Cypher

Cypher

created by: Hideki Tsukamoto
NFTIME
created by: @nftxyz.art
Jalt2
created by: AltJames
smartbags.
created by: Nahiko & dievardump
Rad Impressions
created by: Frank Force
Golly Ghosts
created by: Michael Hirsch