DEV Community

zachrosen
zachrosen

Posted on

A Beginner's Guide to Creating Your First NFT Minting dApp

Hey there!

Want to launch your first mintable NFT collection? Sweet, this guide is for you!
Our goal today is to get you up-and-running with a production-ready NFT minting dApp within an hour.

Formatting here gets janky, so you can view the pretty version here.

Let’s get started by cloning our starter code. Open a fresh folder and run the following in your terminal:
git clone https://github.com/brydge-network/full-stack-nft-mint-tutorial.git
cd full-stack-nft-mint-tutorial

Styling isn’t the focus of our tutorial—we’re more interested in building smart contracts and triggering them. The starter code kicks us off with some cool CSS stuff we’ll use later to make our dApp’s UI look pretty.
Now, time for some contracts!
From here on out, we’ll assume that you’re in the full-stack-nft-mint-tutorial folder.
Run the following in terminal:
brownie init

This scaffolds out our back end structure for us.
Now, create a file called brownie-config.yaml in your root and paste the following:
dotenv: .env
dependencies:
- smartcontractkit/chainlink-brownie-contracts@0.4.0
- OpenZeppelin/openzeppelin-contracts@4.5.0
compiler:
solc:
remappings:
- '@chainlink=smartcontractkit/chainlink-brownie-contracts@0.4.0'
- '@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.5.0'
wallets:
from_key: ${PRIVATE_KEY}

Important parts here are noting our .env file (where we keep sensitive information) and from_key (the wallet we’ll use to deploy our NFTs).
Next, create an .env file in root and paste the following. You can export your private key from MetaMask (use a separate dev wallet!) and get an Infura key for free here. We can leave the rest blank for now
PRIVATE_KEY = ""
WEB3_INFURA_PROJECT_ID=''
ETHERSCAN_CONTRACT_ADDR=''
PINATA_API_KEY=''
PINATA_API_SECRET=''
ETHERSCAN_TOKEN=''

It’s (finally!) smart contract time. Create a file called BrydgeCollection.sol in /contracts and paste in the following code:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import '@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol';
import '@openzeppelin/contracts/token/ERC721/ERC721.sol';

contract BrydgeCollection is ERC721URIStorage {
uint256 public tokenCounter;
//set price to 0.001 native
uint256 price = 1000000000000000;

constructor() public ERC721('Brydge Tutorial NFTs', 'BRYDGE') {
tokenCounter = 0;
}

function mintBrydgeNFT(string memory tokenURI) public payable returns (bytes32) {
require(msg.value >= price, "Send more tokens next time!");
require(tokenCounter < 100, 'Max number of tokens reached');
uint256 tokenId = tokenCounter;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenURI);
tokenCounter++;
}
}

Our NFT contract is super simple. We define our collection name (BrydgeCollection) and set a price to mint. We then set tokenCounter to zero at inception, since we haven’t minted any NFTs yet.
We then expose 1 function to the world: mintBrydgeNFT(). This function takes in a tokenURI (we’ll get to this later) and tokens. If too few tokens are paid, or this contract has already minted 100 NFTs, the transaction fails. Otherwise, it mints a fresh NFT and sends it to the buyer.
Next, we need to compile our contract into machine-readable language. Run the following in your root terminal:
brownie compile

Let’s now deploy our NFTs. Create a file called deploy_nft.py in /scripts and paste in the following:
from brownie import BrydgeCollection, accounts, config

def main():
dev = accounts.add(config['wallets']['from_key'])
BrydgeCollection.deploy(
{'from': dev}
)

This is as simple as can be. We set the “from” wallet to ours. We then deploy BrydgeCollection.
Next, run the following in your root terminal to deploy! We’re deploying on Rinkeby testnet, so head over to the free Rinkeby faucet to get yourself some test ETH to pay deployment gas costs.
brownie run scripts/deploy_nft.py --network rinkeby

If all goes well, we’ll get the following in our terminal.
published

Screen Shot 2022-05-31 at 10.34.29 PM

Copy your deployment address and paste it into search bar in https://rinkeby.etherscan.io/. Boom! We have liftoff!
Now, create an “images” folder in root and add a few images that you want to immortalize. Call them 1.jpg, 2.jpg, 3.jpg for now to keep things simple. Your file structure will look like this
-build
-images
—---1.jpg
—---2.jpg
—---3.jpg
-.env
-client
...

Now, let’s fill out more of our .env. Grab a free API key and secret from Pinata, paste into the proper spots.
Next, create a /metadata folder in your root. This will store our precious NFT data temporarily.
Now, create a file called create_collection_data.py in /scripts and paste the following code:
import requests
import os
import json
from brownie import BrydgeCollection

metadata_template = {
"name": "",
"description": "",
"image": ""
}

def main():
# gets our most recent NFT deployment
brydge_collection = BrydgeCollection[-1]
existing_tokens = brydge_collection.tokenCounter()
#however many nfts we want to deploy
meta_data_hashes = write_metadata(3)

def write_metadata(num_tokens):
# We'll use this array to store the hashes of the metadata
meta_data_hashes = []
for token_id in range(num_tokens):
collectible_metadata = metadata_template.copy()
# The filename where we're going to locally store the metadata
meta_data_filename = f"metadata/{token_id + 1}.json"
# Name of the collectible set to its token id
collectible_metadata["name"] = str(token_id)
# Description of the NFT
collectible_metadata["description"] = f"Brydge NFT #{token_id}"
# Path of the artwork to be uploaded to IPFS
img_path = f"images/{token_id + 1}.jpg"
with open(img_path, "rb") as f:
img_binary = f.read()
# Upload the image to IPFS and get the storage address
image = upload_to_ipfs(img_binary)
# Add the image URI to the metadata
image_path = f"https://ipfs.io/ipfs/{image}"
collectible_metadata["image"] = image_path
with open(meta_data_filename, "w") as f:
# Write the metadata locally
json.dump(collectible_metadata, f)
# Upload our metadata to IPFS
print('collectible', collectible_metadata)

    meta_data_hash = upload_to_ipfs(collectible_metadata['image'])
    print('metadata hash', meta_data_hash)
    meta_data_path = f"<https://ipfs.io/ipfs/{meta_data_hash}>"
    # Add the metadata URI to the array
    meta_data_hashes.append(meta_data_path)
with open('metadata/data.json', 'w') as f:
    # Finally, we'll write the array of metadata URIs to a file
    json.dump(meta_data_hashes, f)
return meta_data_hashes
Enter fullscreen mode Exit fullscreen mode

def upload_to_ipfs(data):
# Get our Pinata credentials from our .env file
pinata_api_key = os.environ["PINATA_API_KEY"]
pinata_api_secret = os.environ["PINATA_API_SECRET"]
endpoint = "https://api.pinata.cloud/pinning/pinFileToIPFS"
headers = {
'pinata_api_key': pinata_api_key,
'pinata_secret_api_key': pinata_api_secret
}
body = {
'file': data
}
# Make the pin request to Pinata
response = requests.post(endpoint, headers=headers, files=body)
# Return the IPFS hash where the data is stored
return response.json()["IpfsHash"]

We first set a template object called metadata_template to standardize how we want to store each NFT’s metadata.
Our main() function kicks off our script by calling write_metadata() with the number of NFTs we want to deploy passed in. This number corresponds to the number of photos you have in /images.
write_metadata() then fills in the metadata_template for each NFT we’re deploying and calls upload_to_ipfs() to write that object to IPFS (our online file storage system that’s a decentralized version of AWS).
Now, let’s run the following command in our root terminal to pin our NFT images to IPFS:
brownie run scripts/create_collection_data.py --network rinkeby

This populates /metadata with our image data. Take a quick look to view your work!
We’re now done with the back end, let’s take a look at front end in /client. Shoutout to Abhinav for creating most of the front end scaffolding we’ll use.
Run the following in your root terminal. This enters our separate front end folder (called client) that came with our starter code, installs our dependencies, and spins up a local developer environment.
cd client
npm install
npm run dev

Remember that NFT deployment address we saved? Drop that into /client/config.js
export const nftContractAddress = 'YourDeployedNFTContractAddress'

Replace contents of /client/pages/index.js with the following:
import { useState } from 'react'
import { nftContractAddress } from '../config.js'
import { ethers } from 'ethers'
import axios from 'axios'

import NFT from '../../build/contracts/BrydgeCollection.json'
import uriList from '../../metadata/data.json'

const MintPage = () => {
// Hooks that render data once variables are set
const [mintedNFT, setMintedNFT] = useState(null)
const [miningStatus, setMiningStatus] = useState(null)
const [currentAccount, setCurrentAccount] = useState('')

// Calls Metamask to connect wallet on clicking Connect Wallet button
const connectWallet = async () => {
    try {
        const { ethereum } = window

        if (!ethereum) {
            console.log('Metamask not detected')
            return
        }
        let chainId = await ethereum.request({ method: 'eth_chainId' })
        console.log('Connected to chain:' + chainId)

        const rinkebyChainId = '0x4'

        const devChainId = 1337
        const localhostChainId = `0x${Number(devChainId).toString(16)}`

        if (chainId !== rinkebyChainId && chainId !== localhostChainId) {
            alert('You are not connected to the Rinkeby Testnet!')
            return
        }

        const accounts = await ethereum.request({ method: 'eth_requestAccounts' })

        console.log('Found account', accounts[0])
        setCurrentAccount(accounts[0])
    } catch (error) {
        console.log('Error connecting to metamask', error)
    }
}

// Creates transaction to mint NFT on clicking Mint button
const mintNFT = async () => {
    try {
        const { ethereum } = window

        if (ethereum) {
            const provider = new ethers.providers.Web3Provider(ethereum)
            const signer = provider.getSigner()
            const nftContract = new ethers.Contract(
                nftContractAddress,
                NFT.abi,
                signer
            )
            // let nftTx = await nftContract.createEternalNFT()
            let nftId = await nftContract.tokenCounter()
            nftId = await nftId.toNumber()
            console.log('about to mint Brydge NFT #', nftId)
            const nftUri = uriList[nftId]
            // const nftUri = uriList[0]
            console.log(nftUri)
            let nftTx = await nftContract.mintBrydgeNFT(nftUri, { value: ethers.utils.parseEther("0.001") })
            console.log('Mining....', nftTx.hash)
            setMiningStatus(0)

            let tx = await nftTx.wait()
            console.log('Mined!', tx)

            console.log(
                `Mined, see transaction: <https://rinkeby.etherscan.io/tx/${nftTx.hash}`>
            )

            getMintedNFT(nftId)
        } else {
            console.log("Ethereum object doesn't exist!")
        }
    } catch (error) {
        console.log('Error minting NFT', error)
    }
}

// Gets the minted NFT data
const getMintedNFT = async (nftId) => {
    try {
        const { ethereum } = window

        if (ethereum) {
            const provider = new ethers.providers.Web3Provider(ethereum)
            const signer = provider.getSigner()
            const nftContract = new ethers.Contract(
                nftContractAddress,
                NFT.abi,
                signer
            )
        //token uri is a storing of our nft's data in machine-readable format
            let tokenUri = await nftContract.tokenURI(nftId)
            let data = await axios.get(tokenUri)
            let image = data.data

            setMiningStatus(1)
            setMintedNFT(image)
        } else {
            console.log("Ethereum object doesn't exist!")
        }
    } catch (error) {
        console.log(error)
    }
}

return (
    <div className='flex flex-col items-center pt-32 bg-[#0B132B] text-[#d3d3d3] min-h-screen'>
        <div className='trasition hover:rotate-180 hover:scale-105 transition duration-500 ease-in-out'>
            <svg
                xmlns='<http://www.w3.org/2000/svg>'
                width='60'
                height='60'
                fill='currentColor'
                viewBox='0 0 16 16'
            >
                <path d='M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z' />
            </svg>
        </div>
        <h2 className='text-3xl font-bold mb-20 mt-12'>
            Mint Brydge NFT
        </h2>
        {currentAccount === '' ? (
            <button
                className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
                onClick={connectWallet}
            >
                Connect Wallet
            </button>
        ) : (
            <button
                className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
                onClick={mintNFT}
            >
                Mint Brydge NFT for 0.001 ETH
            </button>
        )}
        {miningStatus === 1 ? (
            <img
            src={mintedNFT}
            alt=''
            className='h-60 w-60 rounded-lg shadow-2xl shadow-[#6FFFE9] hover:scale-105 transition duration-500 ease-in-out'
        />
        ) : (
            <div></div>
        )}
    </div>
)
Enter fullscreen mode Exit fullscreen mode

}

export default MintPage

Here, we define our React component called MintPage and add a few functions triggered on button click.
We then add some HTML in the return() statement. We’ll see that we check if the user is signed in via Metamask. If not, we’ll show a button for the user to connect their wallet. If so, we’ll allow the user to mint our NFT.
We then have a final check to see if our transaction has been submitted and confirmed on the blockchain (mined). If this is true, we’ll show an image of the NFT we just minted. If not, we’ll show nothing.
Save the file, then head to localhost:3000 in your browser and you’ll see our minting UI.
Click “Connect Wallet” button to connect your MetaMask. Switch to Rinkeby, since that’s where we deployed!
Right click in your browser, click “Inspect”, then click the “Console” tab so we can see some information being logged.
If you’re using the same wallet to mint that you deployed with, you can now click the “Mint” button and watch the magic happen. If not, head back to to https://rinkebyfaucet.com/ to get some more Rinkeby ETH.
Once your transaction is confirmed on the Blockchain, you’ll see your NFT-ified image pop up. Mint again and you’ll see the next NFT in your collection. Check your wallet transaction history on the Rinkeby block explorer and you’ll see each action confirmed on the blockchain.
Want to deploy on a live network so real people can buy + mint your work? Add some real MATIC to your wallet via the free Polygon faucet, run the following in your terminal in /client.
cd ..
brownie run scripts/deploy_nft.py --network polygon-mainnet
brownie run scripts/create_collection_data.py --network polygon-mainnet

And that’s a wrap! You’ve deployed a full-stack NFT minting application on both testnet and mainnet.
This tutorial gets you up-and-running with a collection mintable using the native token on one chain (ETH on Ethereum / Rinkeby and MATIC on Polygon). Each chain brings about pros and cons:
Ethereum

Pros: largest potential customer base + easiest onboarding
Cons: super expensive to deploy ($200+) and mint ($50+) NFTs

Polygon

Pros: <$0.01 transaction fees
Cons: users have to bridge tokens from Ethereum —> Polygon before using your dApp

Brydge provides the best of both worlds, enabling you to deploy on one chain and instantly accept any token from the rest!
So, you can deploy on Polygon to take advantage of sub-1 cent deployment costs and user transaction fees, while seamlessly accepting payments from users on Ethereum. Plus, you can instantly accept USDC, LINK, DAI, and thousands of other tokens without redeploying your contracts!
Learn more at www.brydge.network and view SDK documentation here.
We’re always here to help in our Discord!

Top comments (1)

Collapse
 
codybra profile image
CodyBra • Edited

Thank you so much for such a deep and at the same time clear guide! I recently discovered the world of NFT with the help of artozo.com/ and immediately knew that I wanted to write something like this. I hope I can get a freelance order for something like this!