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.
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.
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.
- Download and install an IDE of your preference. We recommend VSCode.
To do this, navigate to console.filebase.com. If you don’t have an account already, sign up, then log in.
Select ‘Create Bucket’ in the top right corner to create a new bucket for your NFTs.

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.


Once uploaded, it will be listed in the bucket.

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

mkdir nft-tutorial
cd nft-tutorial
npm init
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 [email protected]^5.0.0
npm install --save-dev [email protected]
npx hardhat
mkdir contracts
mkdir scripts
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;
}
}
Select the ‘Create App’ button to get started.

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

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

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

This file must be called .env, otherwise it will not work as expected.
The format of the
.env
file is as follows:ALCHEMY_KEY = "alchemy-api-key"
ACCOUNT_PRIVATE_KEY = "private-key"
/**
* @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}`]
},
},
}
npx hardhat compile
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);
});
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.
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}`);
});
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,
}
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}`]
},
},
}
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}`);
});
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"
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}`]
},
},
}
Use the following command with your NFT contract address to mint your deployed contract:
npx hardhat mint --address [NFT-CONTRACT-ADDRESS]
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;
}
}
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.
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)}`);
});
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.
If you have any questions, please join our Discord server, or send us an email at [email protected]
Last modified 8mo ago