Create an open edition from zero to one

In this blog post we will see how to create a open edition NFT, we will start by creating the smart contract and then the minting page.

Project setup

Next.JS + TailwindCSS + RainbowKit + wagmi

Let's start by creating a new Next.JS project : Run npx create-next-app@latest --ts --src-dir.

Next install the required dependencies and initialize tailwind :

yarn add @rainbow-me/rainbowkit wagmi tailwindcss postcss autoprefixer
npx tailwindcss init -p

Edit the tailwind.config.js file to look like this :

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

And include this in global.css :

@tailwind base;
@tailwind components;
@tailwind utilities;

Finally update the _app.tsx with this code :

import '@/styles/globals.css'
import '@rainbow-me/rainbowkit/styles.css'

import { RainbowKitProvider, getDefaultWallets } from '@rainbow-me/rainbowkit'
import { WagmiConfig, configureChains, createClient } from 'wagmi'

import type { AppProps } from 'next/app'
import { alchemyProvider } from 'wagmi/providers/alchemy'
import { config } from '@/config'
import { publicProvider } from 'wagmi/providers/public'

const { chains, provider } = configureChains(config.chains, [
  alchemyProvider({ apiKey: config.alchemyKey }),
  publicProvider(),
])

const { connectors } = getDefaultWallets({
  appName: config.appName,
  chains,
})

const wagmiClient = createClient({
  autoConnect: false,
  connectors,
  provider,
})

export default function App({
  Component,
  pageProps: { session, ...pageProps },
}: AppProps) {
  return (
    <WagmiConfig client={wagmiClient}>
      <RainbowKitProvider chains={chains}>
        <Component {...pageProps} />
      </RainbowKitProvider>
    </WagmiConfig>
  )
}

If you want more details about the setup, you can check this blog post.

Smart contract setup

Now let's setup the smart contract. In the root directory create a folder named contracts. Inside this folder run the following commands

npm init -y
npm install hardhat @nomiclabs/hardhat-ethers @openzeppelin/contracts dotenv ethers

If you want more detail about this packages and why we use them, you can check this blog post. After run npx hardhat and select "Create an empty hardhat.config.js".

Create the file for the smart contract in a new sub-direcotry contracts/oe.sol and create the script to deploy it scripts/deploy.js.

In scripts/deploy.js add this :

async function main() {
  const MyOE = await ethers.getContractFactory('MyOpenEdition')
  // Start deployment, returning a promise that resolves to a contract object
  const MyOENFT = await MyOE.deploy()
  await MyOENFT.deployed()
  console.log('Contract deployed to address:', MyOENFT.address)
}
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error)
    process.exit(1)
  })

Write the contract

Now let's add the code for the smart contract. In the file contracts/oe.sol add this :

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

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyOpenEdition is ERC1155, Ownable {

    string public name = "MyOpenEdition";
    string public symbol = "MOE";


    constructor() ERC1155("https://gateway.pinata.cloud/ipfs/QmPhjMEXfoDzq6atJ6n2UPjMVZUy1gDT2BiF5bpBkpWudY") {}

    function setURI(string memory newuri) public onlyOwner {
        _setURI(newuri);
    }

    function mint()
        public
    {
        require(balanceOf(msg.sender, 1) == 0 , "Max Mint per wallet reached");
        _mint(msg.sender, 1, 1, "");
    }

    function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data)
        public
        onlyOwner
    {
        _mintBatch(to, ids, amounts, data);
    }
}

Let's analyze the code :

  • First we import the ERC1155 and Ownable contract from OpenZeppelin.

  • Then we create our contract and inherit from ERC1155 and Ownable.

  • We set the name and symbol of the contract.

  • We create a constructor that set the URI of the NFT. The URI is a link to the meta data of the token. As you can see it's hosted via Pinata. If you want to to use your own you will to updload the metadata to Pinata and change the value.

Here the metadata file look like :

{
  "name": "My Open Edition",
  "description": "This is an open edition NFT use for blog post of tibyverse.xyz",
  "image": "https://gateway.pinata.cloud/ipfs/QmQ3BhGDLUcsL2n95PsicCvCkHMzsSCPczRBP1nbdb44t1"
}
  • After we define a setUri function that allow the owner to change the URI of the NFT.
  • We define a mint function that allow to mint a NFT. We check that the user doesn't have already a NFT because we want to allow only one nft per wallet.
  • We define a mintBatch function that allow the owner to mint multiple NFT at one if he want.

For the mint function we can also make the function payable. So the user will need to pay a fee to mint the NFT. Here is the code :

function mint() public payable
{
    require(msg.value >= 0.001 ether , "Not enough ETH sent; check price!");
    require(balanceOf(msg.sender, 1) == 0 , "Max Mint per wallet reached");
    _mint(msg.sender, 1, 1, "");
}

That's pretty much it for the smart contract. You can build it with npx hardhat compile and deploy it with npx hardhat run scripts/deploy.js --network goerli.

Frontend

Now let's build the frontend. As we had previously setup the project we can start by creating the hooks to interact with the smart contract.

In the folder hooks create a file useMint.ts and add this :

import {
  useAccount,
  useContractWrite,
  useEnsName,
  useNetwork,
  usePrepareContractWrite,
  useWaitForTransaction,
} from 'wagmi'
import { useAccountModal, useConnectModal } from '@rainbow-me/rainbowkit'

import { config } from '@/config'

export const useMint = () => {
  const { address, isConnected } = useAccount()
  const { data: ensName } = useEnsName({ address })
  const { openConnectModal } = useConnectModal()
  const { openAccountModal } = useAccountModal()
  const { chain, chains } = useNetwork()

  const {
    config: mintConfig,
    error: prepareError,
    isError: isPrepareError,
  } = usePrepareContractWrite({
    address: config.contract as `0x${string}`,
    abi: [
      {
        name: 'mint',
        type: 'function',
        stateMutability: 'nonpayable',
        inputs: [],
        outputs: [],
      },
    ],
    functionName: 'mint',
  })
  const {
    data,
    write,
    error: mintError,
    isError: isMintError,
  } = useContractWrite(mintConfig)

  const { isLoading, isSuccess, isError } = useWaitForTransaction({
    hash: data?.hash,
  })

  const formattedAdr = address
    ? `${address.substring(0, 6)}${address.substring(
        address.length - 6,
        address.length
      )}`
    : ''

  const displayName = ensName
    ? `${ensName.substring(0, 16)}`
    : `${formattedAdr}`

  return {
    displayName,
    openAccountModal,
    openConnectModal,
    isConnected,
    isSupportedNetwork: chain?.unsupported === false,
    supportedNetwork: chains[0]?.name ?? '',
    mint: write,
    isMinting: isLoading,
    isMinted: isSuccess,
    mintData: data,
    prepareError,
    isPrepareError,
    mintError,
    isMintError,
  }
}

We already use this hooks in this blog post. Once it's done we can create the components :

  • ConnectButton : the custom ConnectWallet button
  • MintButton: the button that will mint the NFT and display error if needed
  • Error : a component to display error message

error.tsx

import React from 'react'

interface ErrorProps {
  error: string | undefined | null
}

export const Error: React.FC<ErrorProps> = ({ error }) => {
  if (error) {
    return <p className="max-w-3xl  break-all text-red-500">{error}</p>
  }
  return null
}

ConnectButton.tsx

import React from 'react'
import { useMint } from '@/hooks/useMint'

export const ConnectButton: React.FC = () => {
  const { openAccountModal, openConnectModal, isConnected } = useMint()

  return (
    <button
      onClick={() =>
        isConnected
          ? openAccountModal && openAccountModal()
          : openConnectModal && openConnectModal()
      }
      type="button"
      className="inline-flex items-center rounded-2xl border border-transparent bg-fuchsia-600 px-6 py-4 text-base font-medium text-white shadow-sm hover:bg-fuchsia-700 focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2"
    >
      Connect Wallet
    </button>
  )
}

And finally MintButton.tsx

import { Error } from './Error'
import React from 'react'
import { config } from '@/config'
import { useMint } from '@/hooks/useMint'

export const MintButton: React.FC = () => {
  const {
    isSupportedNetwork,
    supportedNetwork,
    displayName,
    mint,
    isMinting,
    isMinted,
    mintData,
    isPrepareError,
    isMintError,
    prepareError,
    mintError,
  } = useMint()

  if (!isSupportedNetwork)
    return (
      <Error error={`Wrong network please switch to ${supportedNetwork}`} />
    )

  return (
    <div className="space-y-4">
      <h2 className="text-xl font-extrabold text-gray-900">
        {`Welcome ${displayName}`}
      </h2>
      <button
        type="button"
        disabled={!mint || isMinting}
        onClick={() => mint?.()}
        className="inline-flex items-center rounded-2xl border border-transparent bg-fuchsia-600 px-6 py-4 text-base font-medium text-white shadow-sm hover:bg-fuchsia-700 focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2"
      >
        {isMinting ? 'Minting...' : 'Mint'}
      </button>
      {mintData?.hash ? (
        <div>
          <a
            href={`${config.blockchainExplorerUrl}/tx/${mintData?.hash}`}
            className="text-lg font-bold"
          >
            See transaction
          </a>
        </div>
      ) : null}
      {isMinted && (
        <div>
          <div className="mt-2 text-lg font-bold text-green-500">
            Successfully minted your NFT!
          </div>
        </div>
      )}
      {(isPrepareError || isMintError) && (
        <Error error={(prepareError || mintError)?.message} />
      )}
    </div>
  )
}

We want the app to be reusable with the less code updates possible. So we will create a config file in config/index.ts file that will export all we need to make the page dynamic.

config/index.ts

import { goerli } from 'wagmi'

export const config = {
  alchemyKey: process.env.NEXT_PUBLIC_ALCHEMY_KEY ?? '',
  contract: '0xd669408C66838a45a28d43AA782cf3c92fb7cb78',
  appName: 'The Cozy place',
  title: 'The Cozy place',
  description:
    'In the virtual world, there is a place known as the Woodland Haven. It is said that this place was once a dense forest that was home to a community of peaceful woodland creatures. Over time, the creatures worked together to create a magical place where they could gather and share their knowledge and stories.',
  tag: '0xTiby',
  chains: [goerli],
  blockchainExplorerUrl: 'https://goerli.etherscan.io',
}

And a .env file with the alchemy key.

And now finally we can create the page pages/index.tsx

import { useEffect, useState } from 'react'

import { ConnectButton } from '@/components/ConnectButton'
import Head from 'next/head'
import Image from 'next/image'
import { MintButton } from '@/components/MintButton'
import type { NextPage } from 'next'
import Preview from 'public/nft-preview.png'
import { config } from '@/config'
import { useMint } from '@/hooks/useMint'

const Home: NextPage = () => {
  const [mounted, setMounted] = useState(false)
  const { isConnected } = useMint()
  useEffect(() => setMounted(true), [])

  if (mounted) {
    return (
      <div className="">
        <Head>
          <title>{config.title}</title>
          <meta name="description" content={config.description} />
          <link rel="icon" href="/favicon.ico" />
        </Head>

        <main className="flex min-h-screen w-screen items-center  justify-center bg-emerald-700 px-4 lg:py-12 lg:px-16">
          <div className="flex flex-col rounded-xl bg-white p-6 lg:flex-row lg:items-center lg:justify-between lg:space-x-12 ">
            <div>
              <Image
                src={Preview}
                alt="NFT Preview"
                className="rounded-lg lg:h-[650px] lg:w-[650px]"
              />
            </div>
            <div className="mt-8 flex-1 space-y-16 lg:mt-0">
              <div className="space-y-2">
                <p className="font-semibold text-yellow-400">{config.tag}</p>
                <h1 className="text-5xl font-extrabold text-gray-900 lg:text-7xl">
                  {config.title}
                </h1>
              </div>
              <p className="text-md mt-12 max-w-3xl leading-relaxed text-gray-400">
                {config.description}
              </p>

              {isConnected ? <MintButton /> : <ConnectButton />}
            </div>
          </div>
        </main>
      </div>
    )
  }
  return null
}

export default Home

And that's it! We have a working NFT minting page that let people mint your Open Edition.

As you can see the code let you deploy the same page for multiple collection by just changing few config :

  • change the uri of metadata in the contract constructor
  • update the config file
  • replace the nft-preview.png file with your own

The code is available on github. Feel free to fork it and use it for your own NFTs. You can try it on goerli network here : https://open-edition-nft-template.vercel.app/ If you mint you can check opensea to see your NFT.