🛠️ We partnered with Alchemy to bring you a FREE Ethereum Developer Bootcamp! Get started here 🛠️
In this tutorial, you’ll build an NFT minter and learn how to create a full stack dApp by connecting your smart contract to a React frontend using Metamask and Web3 tools.
One of the greatest challenges for developers coming from a Web2 background is figuring out how to connect your smart contact to a frontend project and interact with it.
By building an NFT minter—a simple UI where you can input a link to your digital asset, a title, and a description —you'll learn how to:
In this tutorial, we will be using React as our frontend framework. Because this tutorial is primarily focused on Web3 development, we won't be spending much time breaking down React fundamentals. Instead, we'll be focusing on bringing functionality to our project
As a prerequisite, you should have a beginner-level understanding of React—know how components, props, useState/useEffect, and basic function calling works. If you've never heard of any of those terms before, you may want to check out this Intro to React tutorial. For the more visual learners, we highly recommend this excellent Full Modern React Tutorial video series by Net Ninja.
Without further ado, let's get started!
Before we even start looking at any code, it's important to understand how making an NFT works. It involves two steps:
1) You publish an NFT smart contract on the Ethereum blockchain. Usually this is an ERC-721 or ERC-1155 smart contract.
The biggest difference between the two NFT smart contact standards is that ERC-1155 is a multi-token standard and includes batch functionality, whereas with the ERC-721 is a single-token standard and therefore only supports transferring one token at a time.
2) You call the minting function on that NFT smart contract to mint the NFT. Minting is simply the act of publishing a unique instance of your non fungible token on the blockchain.
Usually, this minting function requires you to pass in two variables as parameters, first the recipient, which specifies the address that will receive your freshly minted NFT, and second the NFT's tokenURI , a string that resolves to a JSON document describing the NFT's metadata.
An NFT's metadata is really what brings it to life, allowing it to have properties, such as a name, description, image (or different digital asset), and other attributes. Here's an example of a tokenURI, which contains an NFT's metadata.
In this tutorial, we're going to focus on part 2, calling an existing NFT's smart contract minting function using our React UI.
Here's a link to the ERC-721 NFT smart contract we will be calling in this tutorial. If you'd like to learn how we made it, we highly recommend that you check out our other tutorial, "How to Create an NFT."
Cool, now that we understand how making an NFT works, let's clone our starter files!
First, go to the nft-minter-tutorial github repository to get the starter files for this project. Clone this repository into your local environment.
Don't know how to clone a repository? Check out this guide from Github.
When you open this cloned nft-minter-tutorial repository, you'll notice that it contains two folders: minter-starter-files and nft-minter.
Next open your copy of minter-starter-files in to your favorite code editor (at Alchemy, we we're big fans of VSCode), and then navigate into your src folder:
All of the code we'll write will live under the src folder. We'll be editing the Minter.js component and writing additional javascript files to give our project Web3 functionality.
Before we start coding, it's important check out what's already provided for us in the starter files.
Let's start by running the React project in our browser. The beauty of React is that once we have our project running in our browser, any changes we save will be updated live in our browser.
To get the project running, navigate to the root directory of the minter-starter-files folder, and the run npm install in your terminal to install the dependencies of the project:
Once those have finished installing, run npm start in your terminal:
Doing so should open http://localhost:3000/ in your browser, where you'll see the frontend for our project. It should consist of 3 fields: a place to input a link to your NFT's asset, enter the name of your NFT, and provide a description.
If you try clicking "Connect Wallet" or "Mint NFT" buttons, you'll notice they don't work—that's because we still need to program their functionality! :)
NOTE: Make sure you're in the minter-starter-files folder and not the nft-minter folder!
Let's go back into the src folder in our editor and open the Minter.js file. It's super important that we understand everything in this file, as it is the primary React component we will be working on.
At the top of our this file, we have our state variables that we will update after specific events.
Never heard of React state variables or state hooks? Check out these docs.
Here's what each of the variables represent:
After the state variables, you'll see three un-implemented functions: useEffect, connectWalletPressed, and onMintPressed. You'll notice that all of these functions are async, that's because we will be making asynchronous API calls in them! Their names are eponymous with their functionalities:
Near the end of this file, we have the UI of our component. If you scan this code carefully, you'll notice that we update our url, name, and description state variables when the input in their corresponding text fields change.
You'll also see that connectWalletPressed and onMintPressed are called when the buttons with IDs mintButton and walletButton are clicked respectively.
Finally, let's address where is this Minter component added.
If you go to the App.js file, which is the main component in React that acts as a container for all other components, you'll see that our Minter component is injected on line 7.
In this tutorial, we'll only be editing the Minter.js file and adding files in our src folder.
Now that we understand what we're working with, let's set up our Ethereum wallet!
For users to be able to interact with your smart contract they will need to connect their Ethereum wallet to your dApp.
For this tutorial, we’ll use Metamask, a virtual wallet in the browser used to manage your Ethereum account address. If you want to understand more about how transactions on Ethereum work, check out this page from the Ethereum foundation.
You can download and create a Metamask account for free here. When you are creating an account, or if you already have an account, make sure to switch over to the “Ropsten Test Network” in the upper right (so that we’re not dealing with real money).
In order to mint our NFTs (or sign any transactions on the Ethereum blockchain), we’ll need some fake Eth. To get Eth you can go to the Ropsten faucet and enter your Ropsten account address, then click “Send Ropsten Eth.” You should see Eth in your Metamask account soon after!
To double check our balance is there, let’s make an eth_getBalance request using Alchemy’s composer tool. This will return the amount of Eth in our wallet. After you input your Metamask account address and click “Send Request”, you should see a response like this:
NOTE: This result is in wei not eth. Wei is used as the smallest denomination of ether. The conversion from wei to eth is: 1 eth = 10¹⁸ wei. So if we convert 0xde0b6b3a7640000 to decimal we get 1*10¹⁸ which equals 1 eth.
Phew! Our fake money is all there! 🤑
Now that our Metamask wallet is set up, let's connect our dApp to it!
Because we want to prescribe to the M-V-C paradigm, we're going to create a separate file that contains our functions to manage the logic, data, and rules of our dApp, and then pass those functions to our frontend (our Minter.js component).
To do so, let's create a new folder called utils in your src directory and add a file called interact.js inside it, which will contain all of our wallet and smart contract interaction functions.
In our interact.js file, we will write a connectWallet function, which we will then import and call in our Minter.js component.
In your interact.js file, add the following
Let's breakdown what this code does:
First, our function checks if it window.ethereum is enabled in your browser.
window.ethereum is a global API injected by Metamask and other wallet providers that allows websites to request users' Ethereum accounts. If approved, it can read data from the blockchains the user is connected to, and suggest that the user sign messages and transactions. Check out the Metamask docs for more info!
If window.ethereum is not present, then that means Metamask is not installed. This results in a JSON object being returned, where address returned is an empty string, and the status JSX object relays that the user must install Metamask.
Most of the functions we write will be returning JSON objects that we can use to update our state variables and UI.
Now if window.ethereum is present, then that's when things get interesting.
Using a try/catch loop, we'll try to connect to Metamask by calling window.ethereum.request({ method: "eth_requestAccounts" }); Calling this function will open up Metamask in the browser, whereby the user will be prompted to connect their wallet to your dApp.
Now that we've written this connectWallet function, let's connect it to our Minter.js. component.
First, we'll have to import our function into our Minter.js file by adding
import { connectWallet } from "./utils/interact.js"; to the top of the Minter.js file. Your first 11 lines of Minter.js should now look like this:
Then, inside our connectWalletPressed function, we'll call our imported connectWallet function, like so:
Notice how most of our functionality is abstracted away from our Minter.js component from the interact.js file? This is so we comply with the M-V-C paradigm!
In connectWalletPressed, we simply make an await call to our imported connectWallet function, and using its response, we update our status and walletAddress variables via their state hooks.
Now, let's save both files (Minter.js and interact.js) and test out our UI so far.
Open your browser on the http://localhost:3000/ page, and press the "Connect Wallet" button on the top right of the page.
If you have Metamask installed, you should be prompted to connect your wallet to your dApp. Accept the invitation to connect.
You should see that the wallet button now reflects that your address is connected! Yasssss 🔥
Next, try refreshing the page... this is strange. Our wallet button is prompting us to connect Metamask, even though it is already connected...
Don't worry though! We easily can fix that by implementing a function called getCurrentWalletConnected, which will check if an address is already connected to our dApp and update our UI accordingly!
In your interact.js file, add the following getCurrentWalletConnected function:
This code is very similar to the connectWallet function we just wrote earlier.
The main difference is that instead of calling the method eth_requestAccounts, which opens Metamask for the user to connect their wallet, here we call the method eth_accounts, which simply returns an array containing the Metamask addresses currently connected to our dApp.
To see this function in action, let's call it in the useEffect function of our Minter.js component.
Like we did for connectWallet, we must import this function from our interact.js file into our Minter.js file like so:
Now, we simply call it in our useEffect function:
Notice, we use the response of our call to getCurrentWalletConnected to update our walletAddress and status state variables.
Once you've added this code, try refreshing our browser window. The button should say that you're connected, and show a preview of your connected wallet's address - even after you refresh! 😅
The final step in our dApp wallet setup is implementing the wallet listener so our UI updates when our wallet's state changes, such as when the user disconnects or switches accounts.
In your Minter.js file, add a function addWalletListener that looks like the following:
Let's quickly break down what's happening here:
First, our function checks if window.ethereum is enabled (i.e. Metamask is installed).
Finally, we must call it in our useEffect function:
And voila! We've completed programming all of our wallet functionality! Now that our wallet is set up, let's figure out how to mint our NFT!
So remember the NFT metadata we just talked about in Step 0 of this tutorial—it brings an NFT to life, allowing it to have properties, such as a digital asset, name, description, and other attributes.
We're going to need to configure this metadata as a JSON object and store it, so we can pass it in as the tokenURI parameter when calling our smart contract's mintNFT function.
The text in the "Link to Asset", "Name", "Description" fields will comprise the different properties of our NFT's metadata. We'll format this metadata as a JSON object, but there are a couple options for where we can store this JSON object:
To store our metadata on IPFS, we will use Pinata, a convenient IPFS API and toolkit. In the next step, we'll explain exactly how to do this!
If you don't have a Pinata account, sign up for a free account here and complete the steps to verify your email and account.
Navigate to the https://pinata.cloud/keys page, then select the "New Key" button at the top, set the Admin widget as enabled, and name your key.
You'll then be shown a popup with your API info. Make sure to put this somewhere safe.
Now that our key is set up, let's add it to our project so we can use it.
We can safely store our Pinata key and secret in an environment file. Let's install the dotenv package in your project directory.
Open up a new tab in your terminal (separate from the one running local host) and make sure you are in the minter-starter-files folder, then run the following command in your terminal:
Next, create a .env file in the root directory of your minter-starter-files by entering the following on your command line:
This will pop open your .env file in vim (a text editor). To save it hit "esc" + ":" + "q" on your keyboard in that order.
Next, in VSCode, navigate to your .env file and add your Pinata API key and API secret to it, like so:
Save the file, and then you're ready to start writing the function to upload your JSON metadata to IPFS!
Fortunately for us, Pinata has an API specifically for uploading JSON data to IPFS and a convenient JavaScript with axios example that we can use, with some slight modifications.
In your utils folder, let's create another file called pinata.js and then import our Pinata secret and key from the .env file like so:
Next, paste the additional code from below into your pinata.js file. Don't worry, we'll break down what everything means!
So what does this code do exactly?
First, it imports axios, a promise based HTTP client for the browser and node.js, which we will use to make a request to Pinata.
Then we have our asynchronous function pinJSONToIPFS, which takes a JSONBody as its input and the Pinata api key and secret in its header, all to make a POST request to their pinJSONToIPFS API.
As with our connectWallet function return types, we're returning JSON objects so we can use their parameters to update our state variables and UI.
Now that we have a way to upload our NFT metadata to IPFS via our pinJSONToIPFS function, we're going to need a way to load an instance of our smart contract so we can call its mintNFT function.
As we mentioned earlier, in this tutorial we will be using this existing NFT smart contract; however, if you'd like to learn how we made it, or make one yourself, we highly recommend you check out our other tutorial, "How to Create an NFT."
If you examined our files closely, you'll have noticed that in our src directory, there's a contract-abi.json file. An ABI is necessary for specifying which function a contract will invoke as well ensuring that the function will return data in the format you're expecting.
We're also going to need an Alchemy API key and the Alchemy Web3 API to connect to the Ethereum blockchain and load our smart contract. But first, you'll need an Alchemy account.
Once you’ve created an Alchemy account, you can generate an API key by creating an app. This will allow us to make requests to the Ropsten test network.
Navigate to the “Create App” page in your Alchemy Dashboard by hovering over “Apps” in the nav bar and clicking “Create App”
Name your app (we chose "My First NFT!"), offer a short description, select “Staging” for the Environment (used for your app bookkeeping), and choose “Ropsten” for your network.
Click “Create app” and that’s it! Your app should appear in the table below.
Awesome so now that we've created our HTTP Alchemy API URL, copy it to your clipboard like so…
…and then let's add it to our .env file. Altogether, your .env file should look like this:
Now that we have our contract ABI and our Alchemy API key, we're ready to load our smart contract using Alchemy Web3.
First, if you don't have it already, you'll need to install Alchemy Web3 by navigating to the home directory: nft-minter-tutorial in the terminal:
Next let's go back to our interact.js file. At the top of the file, add the following code to import your Alchemy key from your .env file and set up your Alchemy Web3 endpoint:
Alchemy Web3 is a wrapper around Web3.js, providing enhanced API methods and other crucial benefits to make your life as a web3 developer easier. It is designed to require minimal configuration so you can start using it in your app right away!
Next, let's add our contract ABI and contract address to our file.
Once we have both of those, we're ready to start coding our mint function!
Inside your interact.js file, let's define our function, mintNFT, which eponymously will mint our NFT.
Because we will be making numerous asynchronous calls (to Pinata to pin our metadata to IPFS, Alchemy Web3 to load our smart contract, and Metamask to sign our transactions), our function will also be asynchronous.
The three inputs to our function will be the url of our digital asset, name, and description. Add the following function signature below the connectWallet function:
Naturally, it makes sense to have some sort of input error handling at the start of the function, so we exit this function if our input parameters aren't correct. Inside our function, let's add the following code:
Essentially, if any of the input parameters are an empty string, then we return a JSON object where the success boolean is false, and the status string relays that all fields in our UI must be complete.
Once we know our metadata is formatted properly, the next step is to wrap it into a JSON object and upload it to IPFS via the pinJSONToIPFS we wrote!
To do so, we first we need to import the pinJSONToIPFS function into our interact.js file. At the very top of the interact.js, let's add:
Recall, that pinJSONToIPFS takes in a JSON body. So before we make a call to it, we're going to need to format our url, name, and description parameters into a JSON object.
Let's update our code to create a JSON object called metadata and then make a call to pinJSONToIPFS with this metadata parameter:
Notice, we store the response of our call to pinJSONToIPFS(metadata) in the pinataResponse object. Then, we parse this object for any errors.
If there's an error, we return a JSON object where the success boolean is false and our status string relays that our call failed. Otherwise, we extract the pinataURL from the pinataResponse and store it as our tokenURIvariable.
Now it's time to load our smart contract using the Alchemy Web3 API that we initialized at the top of our file. Add the following line of code to the bottom of the mintNFT function to set the contract at the window.contract global variable:
The last thing to add in our mintNFT function is our Ethereum transaction:
If you're already familiar with Ethereum transactions, you'll notice that the structure is pretty similar to what you've seen.
First, we set up our transactions parameters.
Then, we make an await call, window.ethereum.request, where we ask Metamask to sign the transaction. Notice, in this request, we're specifying our eth method (eth_SentTransaction) and passing in our transactionParameters. At this point, Metamask will open up in the browser, and prompt the user to sign or reject the transaction.
Altogether, our mintNFT function should look like this:
That's one giant function! Now, we just need to connect our mintNFT function to our Minter.js component...
Open up your Minter.js file and update the import { connectWallet } from "./utils/interact.js"; line at the top to be:
Finally, implement the onMintPressed function to make await call to your imported mintNFT function and update the status state variable to reflect whether our transaction succeeded or failed:
Ready to take your project live for users to interact with? Check out this tutorial for deploying your Minter to a live website:
JK, you made it to the end of the tutorial! To recap, by building an NFT minter, you successfully learned how to:
And, as always, if you have any questions, we're here to help in the Alchemy Discord. We can't wait to see how you apply the concepts from this tutorial to your future projects! 🧙♂️