Setting a basic project

Directory Structure

Let start organizing the basics of our NFT contract repo! We'll want to start by creating some folders for the contract we will be writing and for the migration scripts that we will use to deploy those contracts

mkdir contracts
mkdir scripts

All Smart Contract code will run in contracts/, whereas scripts/ will contain some JavaScript files we will need to bootstrap the contracts and deploy them later on.

Let's write our first smart contract

Create a new file inside contracts folder called NFT.sol

// 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;
    }
}

There's a lot going on here so let's break down some of the key portions of this code:

  • In Line 1 we define the version of Solidity we want to use

  • Next, from Lines 4-5, we import the necessary contracts from OpenZeppelin to quickly create an implementation of ERC721 without having to "re-invent the wheel" of NFTs. The 2 contracts imported are:

    • ERC721.sol: a 'vanilla' implementation for Non-Fungible Tokens that already implements a ton of useful helper functions.

    • Counters.sol: Provides counters that can only be incremented, decremented or reset that we can use to keep track of total tokens minted as well as quickly get the next tokenId to mint.

  • The next few lines define the NFT contract itself, with Line 7 defining NFT to inherit from ERC721. Note that Solidity contracts support using mixins and can inherit from several different contracts at once. More on that later.

  • Lines 8-9 import and then declare a Counter that we will use to efficiently keep track of the total tokens minted in our contract.

  • Line 11 defines the constructor, which for now simply calls its parent ERC721 constructor and passes in two strings: name and symbol. For now, this constructor is fairly lightweight.

  • Finally, we define the ever-covetted mintTo function. This public function can be called by passing in a valid recipient address in order to mint a new NFT. For now, its very simple:

    • It increments our currentTokenId Counter,

    • Mints the next value of the Counter to the recipient, using OpenZeppelin's nifty _safeMint() private method

    • Finally, it returns the newly minted token's ID back to the caller.

Compiling the contract

First, let's create a new file, .env in the root folder of the project. We'll use this file to store the private key of our account as well as our Alchemy API key.

  • Get your private key from your MetaMask wallet by following the instructions here

  • Get your Alchemy API key here

ALCHEMY_KEY = "alchemy-api-key"
ACCOUNT_PRIVATE_KEY = "private-key"

Next, update the hardhat.config.js file to add the following:

/**
* @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: "rinkeby",
   networks: {
    hardhat: {},
    rinkeby: {
      url: `https://eth-rinkeby.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}`]
    },
  },
}

Compile the contract

npx hardhat compile

Your results:

Compiling 1 file with 0.8.0
Compilation finished successfully

Deploying the contract

Almost at the finish line. All that's left now is to write a simple deploy script in JavaScript and to run it using Hardhat.

Inside the scripts/ folder, create a file called deploy.js and implement it as follows:

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

Our project should now look like this

├── artifacts
│   ├── @openzeppelin
│   │   └── contracts
│   ├── build-info
│   │   └── f179c78b6322d2fddc1e72511467aa46.json
│   └── contracts
│       └── NFT.sol
├── contracts
│   └── NFT.sol
├── hardhat.config.js
├── package-lock.json
├── package.json
└── scripts
    └── deploy.js

8 directories, 6 files

Note: The artifacts directory contains all our compiled contracts and their dependencies.

We are now ready to deploy the contract!

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

You should see a response that looks like this:

Deploying contracts with the account: {ACCOUNT_ADDRESS}
Account balance: 505059205368653101
Contract deployed to address: {NFT_CONTRACT_ADDRESS}

Last updated