Using Pinata and Hardhat

NFT collections, specifically PFP (profile picture) collections, continue to be white-hot. They are attracting interest from the crypto-native crowd as well as from the crypto-newbie crowd. However, Ethereum gas prices have made purchasing NFTs cost-prohibitive for many. Fortunately, there are alternatives to Ethereum that make NFT creation and collection significantly more cost-effective.

While there are many layer 2 scaling solutions for Ethereum, and we’ve written about one of them (Polygon) here, we’re going to focus on a layer 1 solution today. Avalanche is a global financial network for the issuing and trading of all digital goods. One of the features of Avalanche is Ethereum Virtual Machine (EVM) compatibility. This means the smart contracts written for the Ethereum blockchain can be used on Avalanche, but with lower costs and more speed.

Today, we’re going to create a generative NFT collection that uses Avalanche to mint tokens and IPFS to store token metadata and assets. We’ll be using Pinata to make sure all those assets are loaded quickly and reliably.

Getting Started

For this project, we’re going to need to make sure we have the following installed:

That’s it! You can use any text editor you’d like, and while we will be installing other dependencies, this is all you’ll need to get started.

Since we’re using Pinata to upload and fetch content from IPFS, you’ll want to sign up for an account here. You can use the free plan for getting your feet wet, but if you’re working on a mainnet project and don’t want to stand up your own infrastructure, you should consider a paid plan.

Additionally, we’re going to make use of Hardhat for handling our smart contract functionality. Hardhat includes a bunch of functionality that makes writing, testing, and deploying Ethereum contracts easier.

But wait…

We’re working with Avalanche, not Ethereum. Because Avalanche is EVM-compatible, Hardhat works nearly identically with Avalanche as it does Ethereum.

The web3 world moves fast, so if any of the commands below are out of date, be sure to refer to the Hardhat documentation directly.

Installation

The first thing we need to do is create our project repository. We’ll be using a monorepo to create our contract and our frontend app. To create the project, fire up your command-line tool and change into the directory where you keep all your fancy blockchain projects—or wherever you want.

From there, create a new directory and change into it:mkdir avalanche-drop && cd avalanche-drop

Next, we need to initialize the project directory so that we can install Node modules:npm init -y

Now, we can install Hardhat as a development dependency:npm i -D hardhat @nomiclabs/hardhat-waffle

With all our Hardhat and Hardhat-related dependencies installed, we need to create a new Hardhat project within our directory. This is as simple as running:npx hardhat

NPX is a command that allows us to use NPM modules without having to install them globally first. It will ensure we’re using the latest version of the projects.

You’ll be prompted to select from a starter project or an empty project. Choose the empty project option. With that, you are ready to build your project. Let’s dive in together.

The Smart Contract

There are thousands of tutorials out there by now on writing Ethereum NFT contracts, including many by me. So, in this section, we’re going to move fast. All of the code will be there, but we’re not going to spend a ton of time going over it.

The contract will be an ERC721 contract that makes use of the baseURI functionality. It will have a mint price factored in, and it will have a max supply. But it will not have whitelist functionality and any of the other complexities that often go into NFT drops. For that, you should study successful projects. If they are a good project, they have published the verified source code of their contract for you to review on Etherscan. Also check out https://snowtrace.io, which is an implementation of Etherscan custom-built for the Avalanche blockchain.

Ok, let’s do this.

Create a folder in the root of your project called contracts. Inside that folder, add a new file called NFT.sol. Before we move on, we’re going to need to install another dependency. We’re going to install OpenZeppelin’s smart contracts library. Why? Because they have put a ton of time, effort, and money into their contracts. Their contracts are audited while ours won’t be.

Back at the command line, run:npm install @openzeppelin/contracts

Once installed, we can import OpenZeppelin’s libraries to not only make our contract safer but make our life easier. To keep things simple, I’m including the finished contract below:

This contract has two functions: mint and an override function for _baseURI. The override function just sets the baseURI to whatever we decide when we deploy the contract.

Ok, now you know how to structure your contract, but we still need to create the NFT collection and get this smart contract deployed and operable on Avalance.

It would take an entire tutorial (and one that is begging to be written) to cover how the assets for NFT collections are created. So, for the sake of brevity, we’re going to use an example that is already created. It’s a collection of 10 Pinnie the Pinata PFPs. These are not licensed for use in anyone’s project so don’t go trying to make a million bucks off Pinnie, k?

Creating the Collection on IPFS

Once you have your folder of images, make sure each image is named according to incremental token IDs (i.e. 1.png, 2.png, etc). Before we can create our metadata, we need to have the images uploaded to IPFS. This is how we can refer to them in our metadata. To keep things simple, we’ll upload the entire folder.

Fortunately, there’s a simple command-line tool we can use to upload folders (full disclosure, I created the tool): https://www.npmjs.com/package/pinata-upload-cli

You’ll want to install this tool globally so it can run from anywhere. To do so, run:npm i -g pinata-upload-cli

When that’s done installing, you’ll need to authenticate. If at any point you need help with the available command for the CLI tool, you can run pinata-cli -h. To authenticate, you’ll need a Pinata API Key. Sign into your Pinata account, and go to the API Keys page. Generate a new admin key, then copy the JWT version of the key.

Next, you’ll use that JWT to authenticate the Pinata CLI tool like this:pinata-cli -a YOUR_JWT_HERE

You’ll see a message that authentication was successful. That lets you know you’re ready to upload. To upload, you just have to tell the CLI tool where your folder is. Make sure you’re in the root of your project and run the following command:pinata-cli -u ./Pinnies

Of course, change Pinnies to whatever the name of your image folder is. This will start the upload process. You’ll see the process tracked in the CLI output:

When the process is complete, you’ll see an IPFS hash (CID) returned. This is the CID for your folder of images on IPFS. Every image is accessible by visiting either a public IPFS gateway or a Dedicated Gateway if you signed up for the paid plan. The format is like this:

GATEWAY_URL/ipfs/FOLDER_CID/FILE_NAME

So to load image named 1.png you would replace FILE_NAME with that. This is important because we’re going to use this information in our metadata.

Let’s go ahead and create our metadata now. In a generative project, your layers would generally dictate your attributes array in the metadata. For this, I’m going to leave that blank. Attributes are very specific to every project, so trying to set them here would probably cause more confusion. That said, you can read more about ERC721 and ERC1155 metadata attributes here.

Back in the root of your project, create a new file called metadata-generator.js. You’ll also want to create a folder called metadata in the root of your project. Open the metadata-generator.jsfile up in your text editor and add the following:const fs = require('fs');const imageDir = fs.readdirSync("./Pinnies");imageDir.forEach(img => {
 const metadata = {
   name: `Pinnie ${img.split(".")[0]}`,
   description: "A Pinnie in the Limited Pinata Family Collection",
   image: `ipfs://FOLDER_CID/${img.split(".")[0]}.png`,
   attributes: []
 }  fs.writeFileSync(`./metadata/${img.split(".")[0]}`, JSON.stringify(metadata))
});

This code is going to use Node.js’d built-in file-system module to read our directory of images. We are reading the file names from within our images folder. We then loop through the image names in that directory to help create our metadata.

If you look at the image property in the metadata, you can see that we’re using the IPFS protocol URI. This is a future-proof method of referring to the file that is compatible with NFT marketplaces.

Finally, we write this metadata to the directory we created just before called metadata. An important thing to note here is that we don’t add an extension to our metadata files. This is because the default way for ERC721 contracts to work with baseURIs is to just append a token ID but not .json extensions. This can be overridden manually in your contract, but we’re not doing that here.

To run this, return to the command line. Make sure you’re in the root of your project folder and run node metadata-generator.js. When the process is done, you will now have metadata files in your metadata folder.

We need to get this folder of metadata uploaded to IPFS so that we can create our NFT collection on Avalance. Using the same CLI tool, let’s do that.

Again from the root of the project folder, in the command line, run:pinata-cli -u ./metadata

When the upload is complete, the CID returned is what you’ll need for your smart contract. So keep it handy.

Deploying the Contract

It’s time to return to our smart contract. We need to connect our hardhat configuration to the Avalanche testnet (you’ll be able to use the same format for connecting to mainnet), then we need to write a script to deploy the contract.

We’re going to be using the Fuji testnet on Avalance. Back in your project folder, open up your hardhard.config.js file and update it to look like this:require("@nomiclabs/hardhat-waffle");const AVALANCHE_TEST_PRIVATE_KEY = "PRIVATE_KEY_FOR_FUJI";module.exports = {
 solidity: "0.8.0",
 networks: {
   avalancheTest: {
   url: 'https://api.avax-test.network/ext/bc/C/rpc',
   gasPrice: 225000000000,
   chainId: 43113,
   accounts: [`0x${AVALANCHE_TEST_PRIVATE_KEY}`]
 }
}};

The Solidity version in the config will need to match the version OpenZeppelin’s contract libraries are using. At the time of this post, it’s version 0.8.0.

We’ll need to get a private key for a wallet to use with Avalanche’s testnet. I like to do this through Metamask, so that’s what we’ll do. In the Metamask UI, where you can select a network, choose Add a Network at the bottom:

You can add the network info on the page that pops open:

Check the URL. It should look like this: chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/home.html#settings/networks/add-network

For the Network Name, give it a name that makes sense. For me, I named it Avalanche Fuji. For the RCP URL, paste in the value for the testnet from your hardhat.config.js file. Finally, paste in the chain ID from the hardhat.confid.js file.

Click save and you’ll be connected to the Avalanche Fuji testnet. Before we request funds and export our private key, I recommend creating a new wallet. This tutorial does not focus on operational security, but when testing, it’s always a good idea to have separate wallets from anything you would use on the mainnet.

When you’re ready, you’ll need to get the private key for the wallet. To export a private key from Metamask, you’ll need to go into your Account Details. You’ll see an export private key button. Use this to export the key. Remember, you should be using a test wallet for safety.

Once you have exported the key, you can use it in the hardhat.config.js file. Just make sure you DO NOT COMMIT THIS FILE. If it’s committed to Github or any other hosted source control service, the key will almost certainly be scraped.

We will want to fund this test address with some testnet AVAX. You can do so here.

Now, we can write our deploy script to deploy the smart contract. Let’s create a folder called scripts. Inside that folder, create a file called deploy.js. In that new file, add the following:

Remember the CID you received after uploading the metadata folder to Pinata? We’ll need that now. Replace the YOUR_METADATA_FOLDER_CID part of the BASE_URI with that value.

Next, give your token a name and a symbol. This makes it easier to identify on block explorers. Finally, set your NFT mint price. This is denominated in WEI (AVAX WEI, to be exact). In my example above, the mint price would be 0.5 AVAX.

We are using Hardhat, which also installed the Ethers library, to get an instance of our contract. If you named your contract anything different that AvalanceNFTDrop be sure to put that name instead.

The script will grab the compiled contract, create a deployment transaction, and wait for the deployment to be confirmed. The command line will then provide you the smart contract address.

Ready to deploy?! Let’s get this contract onto the Avalance Fuji network. We would want to set up tests before ever deploying a real project. You can read more about testing with Hardhat here. But for this tutorial, we’re going to compile the contract and deploy. So first, run this command:npx hardhat compile

If you see any errors, be sure to review the message. Hardhat is VERY good with its error messages. You should be able to resolve them easily.

Now that our contract is compiled, we can deploy it with this command:npx hardhat run --network avalancheTest scripts/deploy.js

Notice that we’re telling Hardhat to use the avalancheTest network? This points to the naming convention we use in the hardhat.config.js file. If all went well, you should see the contract address printed in the command line. You can take that address and look it up here.

Congratulations! You just deployed an NFT collection contract on Avalanche’s testnet. But we should probably write a script to mint an NFT, right?

Minting

The final thing we’ll do in this tutorial is mint an NFT so that we can see the contract is working correctly. We’ll verify that the right tokenURI is returned for our NFT, and we’ll make sure the image is correct. For fun, we’ll also walk through how you can use another free tool from Pinata to easily convert any IPFS protocol URI format to load the file through the gateway of your choice.

So, the first step is to create a new file in the script folder. Let’s call it mint.js. Inside that file, we’re going to create a script that is similar to the deploy script but with a call to mint an NFT:

As you can see, we still load the compiled contract the same way. We then need to attach our deployed contract’s address. Once we’ve done that, we can call the mint function and pass in our wallet address.

Let’s keep it simple and use the same test wallet we used for deployment. Go ahead and open up Metamask and copy that test wallet address. Pass it in as the argument to the mint function.

Now, we can run the mint script like this:npx hardhat run --network avalancheTest scripts/mint.js

When this is complete, you should see the token ID returned. You just minted an NFT on Avalanche!

How do we know this though? We should probably verify by looking up the tokenURI. To do this, create one more file in your scripts folder and call it tokenUri.js. In that folder, add the following:

With that done, we can run the following command:npx hardhat run --network avalancheTest scripts/tokenUri.js

This should return the string value for the ID you pass in. You’ll notice in the script we passed in 1. That’s because we know only one token has been minted.

Now, we can take the CID value and load it through a public IPFS gateway or through your Dedicated Gateway if you signed up for a paid plan. The format would be:

GATEWAY_URL/ipfs/CID/1 or whatever the tokenID is.

This will resolve to a JSON file that includes your image property. You can then test that the image loads the same way. For example, here is the metadata for my token ID number 1:

https://polluterofminds.mypinata.cloud/ipfs/QmcRuS8GVjko9APurB7pALBCzXJAETyfSujNA26BUvBebb/1

And here is the resulting image:

https://polluterofminds.mypinata.cloud/ipfs/QmUF4cQ27u77FURfDiBipmizKAGrV4jAPVrGip6boDywNz/1.png

Now, you had to manually swap the CID our from the IPFS protocol URI format to the gateway format. Pinata has a tool that will help with this.

@pinata/ipfs-gateway-tools
IPFS Gateway Tools This toolkit contains helper functions for working with IPFS gateway URLs and transforming them as…

Wrapping Up

This guide did not include a couple of important things: generating your NFT assets and connecting your smart contract to the front-end. These would both make for separate tutorials of their own, and I hope to write those soon.

However, I hope this guide has helped you understand how to use Avalanche for your next NFT project.