Hardhat: Creating an NFT Contract using Hardhat and Setting NFT Metadata using IPFS on Filebase

Learn how to create an NFT contract using Hardhat and set the NFT metadata using IPFS on Filebase.

What is Hardhat?

Hardhat is a local Ethereum network designed for development and deployment of smart contracts. Hardhat allows you to deploy and run tests using Solidity and replicate Ethereum network environments without using real cryptocurrency or paying gas fees.

What is Metadata?

Metadata is simply data that provides additional data about other data. Metadata of an object stored on Filebase typically includes the owner’s account email, size, ETag, object key or name, time and date last modified, and if the object is stored on an IPFS bucket, the IPFS CID is included in the object’s metadata.

In this guide, we’ll use Hardhat to create a test environment for creating a smart contract, then deploy and mint that contract to be used with an NFT token. Lastly, we’ll edit the contract to include metadata when we mint NFTs, and in this metadata we’ll include an image URL that uses the IPFS CID that we get from uploading an image to a Filebase IPFS bucket.

Prerequisites:

1. First, we need an image to use as an NFT. We’ll start by uploading an image to Filebase for us to use.

To do this, navigate to console.filebase.com. If you don’t have an account already, sign up, then log in.

2. Select ‘Buckets’ from the left side bar menu, or navigate to console.filebase.com/buckets.

Select ‘Create Bucket’ in the top right corner to create a new bucket for your NFTs.

3. Enter a bucket name and choose the IPFS storage network to create the bucket.

Bucket names must be unique across all Filebase users, be between 3 and 63 characters long, and can contain only lowercase characters, numbers, and dashes.

4. Next, select the bucket from your list of buckets, then select ‘Upload’ in the top right corner to upload an image file.

5. Select an image to be uploaded.

Once uploaded, it will be listed in the bucket.

6. Click on your uploaded object to display the metadata for the object.

Take note of the IPFS CID. We will reference this later.

7. Open a command line interface and create a new directory for your project with the commands:

mkdir nft-tutorial

cd nft-tutorial

8. Initialize your npm configuration if you have not previously done so with the command:

npm init

9. Install npm dependencies for this project with the commands:

npm install --save-dev hardhat

npm install --save-dev @nomiclabs/hardhat-ethers

npm install --save-dev @nomiclabs/hardhat-etherscan

npm install @openzeppelin/contracts

npm install dotenv --save

npm install --save-dev ethers@^5.0.0

npm install --save-dev node-fetch@2

10. Next, initialize Hardhat with the command:

npx hardhat

11. Create two new directories for contracts and scripts with the commands:

mkdir contracts

mkdir scripts

12. Open your preferred IDE and create a new file inside the contracts directory.

Name this file NFT.sol. Enter the following content to the new file:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFT is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private currentTokenId;
    
    constructor() ERC721("NFTTutorial", "NFT") {}
    
    function mintTo(address recipient)
        public
        returns (uint256)
    {
        currentTokenId.increment();
        uint256 newItemId = currentTokenId.current();
        _safeMint(recipient, newItemId);
        return newItemId;
    }
}

13. Now, let’s head over to Alchemy and either sign up for an account or login.

14. From the Alchemy dashboard, we need to create an app.

Select the ‘Create App’ button to get started.

15. Create a new app.

For our example, we called our app NFTs. Set the environment to ‘Staging’, the chain to ‘Ethereum’, and the network to ‘Ropsten’.

16. Next, we need an Ethereum wallet.

For this, we’ll need a Metamask account. You can sign up for one for free here. For additional information on how Ethereum transactions work, check out the Ethereum foundation’s information page here.

17. Once you have a Metamask account, change the network to the ‘Ropsten Test Network’ in the top right corner.

This is so we aren’t dealing with actual currency.

18. Next we’ll need some test currency in our wallet to cover the gas fees for the contract creation.

To do this, we’ll need to go to the Ropsten faucet, enter our wallet address, and click ‘Send Ropsten Eth’.

19. Then, create a file called .env and add your Alchemy API URL and Metamask private key to this file.

This file must be called .env, otherwise it will not work as expected.

  • For instructions on getting your Metamask private key, see here.

  • For instructions on how to get your Alchemy API URL, see here.

The format of the .env file is as follows:

ALCHEMY_KEY = "alchemy-api-key"

ACCOUNT_PRIVATE_KEY = "private-key"

20. Next, update the hardhat.config.js file with the following content:

/**
* @type import('hardhat/config').HardhatUserConfig
*/

require('dotenv').config();
require("@nomiclabs/hardhat-ethers");

const { ALCHEMY_KEY, ACCOUNT_PRIVATE_KEY } = process.env;

module.exports = {
   solidity: "0.8.0",
   defaultNetwork: "ropsten",
   networks: {
    hardhat: {},
    rinkeby: {
      url: `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
    ethereum: {
      chainId: 1,
      url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
  },
}

21. Now let’s compile the contract with the command:

npx hardhat compile

22. Now that the contract is compiled, we need to deploy it.

To do this, we’ll need to create a file in the scripts directory called deploy.js. Enter the following content in this new file:

async function main() {

    // Get our account (as deployer) to verify that a minimum wallet balance is available
    const [deployer] = await ethers.getSigners();

    console.log(`Deploying contracts with the account: ${deployer.address}`);
    console.log(`Account balance: ${(await deployer.getBalance()).toString()}`);

    // Fetch the compiled contract using ethers.js
    const NFT = await ethers.getContractFactory("NFT");
    // calling deploy() will return an async Promise that we can await on 
    const nft = await NFT.deploy();

    console.log(`Contract deployed to address: ${nft.address}`);
}

main()
.then(() => process.exit(0))
.catch((error) => {
    console.error(error);
    process.exit(1);
});

23. We can now deploy the contract with the following command:

npx hardhat run scripts/deploy.js --network rinkeby

Take note of the value that gets returned. This is your contract address, we’ll reference this later. You’ve successfully deployed your contract! Now let’s move onto minting our new contract.

24. Edit the existing deploy.js script so it reflects the following:

const { task } = require("hardhat/config");
const { getAccount } = require("./helpers");

task("check-balance", "Prints out the balance of your account").setAction(async function (taskArguments, hre) {
    const account = getAccount();
    console.log(`Account balance for ${account.address}: ${await account.getBalance()}`);
});

task("deploy", "Deploys the NFT.sol contract").setAction(async function (taskArguments, hre) {
    const nftContractFactory = await hre.ethers.getContractFactory("NFT", getAccount());
    const nft = await nftContractFactory.deploy();
    console.log(`Contract deployed to address: ${nft.address}`);
});

25. We also need to create a new script in our scripts directory called helpers.js.

In this script, enter the following content:

const { ethers } = require("ethers");

// Helper method for fetching environment variables from .env
function getEnvVariable(key, defaultValue) {
    if (process.env[key]) {
        return process.env[key];
    }
    if (!defaultValue) {
        throw `${key} is not defined and no default value was provided`;
    }
    return defaultValue;
}

// Helper method for fetching a connection provider to the Ethereum network
function getProvider() {
    return ethers.getDefaultProvider(getEnvVariable("NETWORK", "ropsten"), {
        alchemy: getEnvVariable("ALCHEMY_KEY"),
    });
}

// Helper method for fetching a wallet account using an environment variable for the PK
function getAccount() {
    return new ethers.Wallet(getEnvVariable("ACCOUNT_PRIVATE_KEY"), getProvider());
}

module.exports = {
    getEnvVariable,
    getProvider,
    getAccount,
}

26. We also need to edit our hardhat.config.js configuration file to reflect the newly defined tasks we created in the scripts above.

Edit your hardhat.confg.js file to reflect the following configuration:

/**
* @type import('hardhat/config').HardhatUserConfig
*/

require('dotenv').config();
require("@nomiclabs/hardhat-ethers");
require("./scripts/deploy.js");

const { ALCHEMY_KEY, ACCOUNT_PRIVATE_KEY } = process.env;

module.exports = {
   solidity: "0.8.0",
   defaultNetwork: "ropsten",
   networks: {
    hardhat: {},
    ropsten: {
      url: `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
    ethereum: {
      chainId: 1,
      url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
  },
}

27. Now let’s add a minting task. Create a new script in the scripts directory called mint.js and enter the following content to create your minting task:

const { task } = require("hardhat/config");
const { getContract } = require("./helpers");

task("mint", "Mints from the NFT contract")
.addParam("address", "The address to receive a token")
.setAction(async function (taskArguments, hre) {
    const contract = await getContract("NFT", hre);
    const transactionResponse = await contract.mintTo(taskArguments.address, {
        gasLimit: 500_000,
    });
    console.log(`Transaction Hash: ${transactionResponse.hash}`);
});

28. Now we need to edit our .env file to reflect our NFT contract address that we took note of when we deployed our contract.

Edit your .env file so it resembles the following:

ALCHEMY_KEY = "alchemy-api-key"

ACCOUNT_PRIVATE_KEY = "private-key"

NETWORK="ropsten"

NFT_CONTRACT_ADDRESS="nft-contract-address"

29. Let’s edit our helpers.js script to include a helper function called getContract() to read the new environment variable we just added to our .env file.

Edit your helpers.js script so it looks like the following:

const { ethers } = require("ethers");
const { getContractAt } = require("@nomiclabs/hardhat-ethers/internal/helpers");

// Helper method for fetching environment variables from .env
function getEnvVariable(key, defaultValue) {
    if (process.env[key]) {
        return process.env[key];
    }
    if (!defaultValue) {
        throw `${key} is not defined and no default value was provided`;
    }
    return defaultValue;
}

// Helper method for fetching a connection provider to the Ethereum network
function getProvider() {
    return ethers.getDefaultProvider(getEnvVariable("NETWORK", "ropsten"), {
        alchemy: getEnvVariable("ALCHEMY_KEY"),
    });
}

// Helper method for fetching a wallet account using an environment variable for the PK
function getAccount() {
    return new ethers.Wallet(getEnvVariable("ACCOUNT_PRIVATE_KEY"), getProvider());
}

// Helper method for fetching a contract instance at a given address
function getContract(contractName, hre) {
    const account = getAccount();
    return getContractAt(hre, contractName, getEnvVariable("NFT_CONTRACT_ADDRESS"), account);
}

module.exports = {
    getEnvVariable,
    getProvider,
    getAccount,
    getContract,
}

We also need to edit our hardhat.config.js configuration file again so that our new mint.js script is included:

/**
* @type import('hardhat/config').HardhatUserConfig
*/

require('dotenv').config();
require("@nomiclabs/hardhat-ethers");
require("./scripts/deploy.js");
require("./scripts/mint.js");

const { ALCHEMY_KEY, ACCOUNT_PRIVATE_KEY } = process.env;

module.exports = {
   solidity: "0.8.0",
   defaultNetwork: "ropsten",
   networks: {
    hardhat: {},
    ropsten: {
      url: `https://eth-ropsten.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
    ethereum: {
      chainId: 1,
      url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_KEY}`,
      accounts: [`0x${ACCOUNT_PRIVATE_KEY}`]
    },
  },
}

30. Let’s mint our NFT now!

Use the following command with your NFT contract address to mint your deployed contract:

npx hardhat mint --address [NFT-CONTRACT-ADDRESS]

31. Lastly, let’s add some metadata to our NFTs.

This includes the name, description, and image that we uploaded to our IPFS Filebase bucket in the beginning of this guide. To do this, we’ll need to edit our NFT.sol contract to resemble the following:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFT is ERC721 {
  using Counters for Counters.Counter;

  Counters.Counter private currentTokenId;

  /// @dev Base token URI used as a prefix by tokenURI().
  string public baseTokenURI;

  constructor() ERC721("NFTTutorial", "NFT") {
    baseTokenURI = "";
  }

  function mintTo(address recipient) public returns (uint256) {
    currentTokenId.increment();
    uint256 newItemId = currentTokenId.current();
    _safeMint(recipient, newItemId);
    return newItemId;
  }

  /// @dev Returns an URI for a given token ID
  function _baseURI() internal view virtual override returns (string memory) {
    return baseTokenURI;
  }

  /// @dev Sets the base token URI prefix.
  function setBaseTokenURI(string memory _baseTokenURI) public {
    baseTokenURI = _baseTokenURI;
  }
}

32. Then, create a metadata file for our NFT.

We’ll call this file 1, which does not require a file extension. In this file, paste the following content:

{
    "name" : "Filebase Robot"
    "description" : "The cute, little Filebase robot!",
    "image" : "https://ipfs.filebase.io/ipfs/[IPFS-CID]", 
    "external_url": "https://example.com/?token_id=1",
 }

Edit the values in this file to reflect your desired configuration. Replace the name, description, and IPFS-CID values. The IPFS CID value is the value we took note of after uploading our image file to our Filebase bucket at the start of this guide.

You will need to upload this metadata file to your Filebase IPFS bucket in the same way that we used to upload our image file to our Filebase bucket. Copy the IPFS CID for the metadata file once uploaded to Filebase.

33. Now we need to edit our mint.js script to include a function that sets this metadata.

Edit your mint.js file to reflect the following:

const { task } = require("hardhat/config");
const { getContract } = require("./helpers");
const fetch = require("node-fetch");

task("mint", "Mints from the NFT contract")
.addParam("address", "The address to receive a token")
.setAction(async function (taskArguments, hre) {
    const contract = await getContract("NFT", hre);
    const transactionResponse = await contract.mintTo(taskArguments.address, {
        gasLimit: 500_000,
    });
    console.log(`Transaction Hash: ${transactionResponse.hash}`);
});

task("set-base-token-uri", "Sets the base token URI for the deployed smart contract")
.addParam("baseUrl", "The base of the tokenURI endpoint to set")
.setAction(async function (taskArguments, hre) {
    const contract = await getContract("NFT", hre);
    const transactionResponse = await contract.setBaseTokenURI(taskArguments.baseUrl, {
        gasLimit: 500_000,
    });
    console.log(`Transaction Hash: ${transactionResponse.hash}`);
});

task("token-uri", "Fetches the token metadata for the given token ID")
.addParam("tokenId", "The tokenID to fetch metadata for")
.setAction(async function (taskArguments, hre) {
    const contract = await getContract("NFT", hre);
    const response = await contract.tokenURI(taskArguments.tokenId, {
        gasLimit: 500_000,
    });
    
    const metadata_url = response;
    console.log(`Metadata URL: ${metadata_url}`);

    const metadata = await fetch(metadata_url).then(res => res.json());
    console.log(`Metadata fetch response: ${JSON.stringify(metadata, null, 2)}`);
});

34. Finally, we’re ready to put it all together. Let’s start by compiling our project:

npx hardhat compile

Then deploy our contract:

npx hardhat deploy

Then set your NFT_CONTRACT_ADDRESS env variable in your .env file. Next, set your metadata file:

npx hardhat set-base-token-uri --base-url "https://METADATA-IPFS-CID.ipfs.dweb.link"

Replace the METADATA-IPFS-CID value with the IPFS CID you copied after uploading your metadata file to your IPFS Filebase bucket.

Then mint your contract:

npx hardhat mint --address [NFT-CONTRACT-ADDRESS]

Then lastly, retrieve your NFT token and it’s metadata:

npx hardhat token-uri --token-id 1

From here, you can move this configuration to a live network such as the Ethereum network by simply creating a new Alchemy app on the Ethereum network and updating your .env file with the new Alchemy API key and changing the network value to ethereum instead of rinkeby.

Keep in mind that once you move to the Ethereum network, you’ll be working with real cryptocurrency and you’ll be charged gas fees for transactions.

Last updated