Home

Blog

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

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

June 2, 2022

Hey there!

Want to launch your first NFT 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.

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.

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 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> ) }


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!

Make your dApp accessible to retail

Make your dApp accessible to retail

Make your dApp accessible to retail