How to Create and Deploy an XRC721 NFT Using Hardhat

Use Hardhat to deploy an XRC721 Token.

🧭 Table of contents

📰 Overview

Hardhat is a development environment to compile, deploy, test, and debug your Ethereum software. Get Solidity stack traces & console.log.

What you will learn

In this tutorial, you will learn how to set up Hardhat and use it to build, test, and deploy a XRC721 token on both the XDC Network mainnet and XDC Apothem testnet.

What you will do

  • Install and setup Hardhat

  • Create an XRC721 token

  • Compile the XRC721 token

  • Deploy the XRC721 token

  • Interact with the XRC721 token

  • Check the deployment status on xinfin.network

📰 About XRC721 Tokens

XRC721 is an open standard that defines an interface for non-fungible tokens on XDC blockchain:

  • balanceOf(address _owner) external view returns (uint256)

  • ownerOf(uint256 _tokenId) external view returns (address)

  • safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable

  • safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable

  • transferFrom(address _from, address _to, uint256 _tokenId) external payable

  • approve(address _approved, uint256 _tokenId) external payable

  • setApprovalForAll(address _operator, bool _approved) external

  • getApproved(uint256 _tokenId) external view returns (address)

  • isApprovedForAll(address _owner, address _operator) external view returns (bool)

These are the minimum required methods that allow an asset on the XDC network to be called an XRC721 token. Also, a XRC721 token must be able to emit the following Events on the blockchain:

  • Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId)

  • Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId)

  • ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved)

Events are helpers that come in handy in the exhaustive process of indexing state changes, and they are essential for off-chain applications to find relevant data on the blockchain. By mapping all Transfer events, for example, we can fetch all the historic data on token transfers more easily.

XRC721 also includes optional metadata parameters:

  • name

  • symbol

This allows your smart contract to be interrogated for its name and for details about the assets that your NFTs represent.

⚒ Starting a new Hardhat Project

There are a few technical requirements before we start. Please install the following:

Start by setting up tour folder. As we are creating a project called XRC721, create a new XRC721 folder by running the following on terminal:

mkdir XRC721 && cd XRC721

You can get started with Hardhat by running:

npx hardhat

The following message will show on your console. Hit y to continue or just press ENTER:

Need to install the following packages:
  hardhat
Ok to proceed? (y)

The following message should log on your console:

Press ENTER to get started with a new JavaScript Hardhat Project. Then you will be presented with the following options:

? Hardhat project root: ‣ /home/taurinos/xdc_comm/docs/how-to/XRC721/Hardhat/XRC721 
// Press ENTER or y 

? Do you want to add a .gitignore? (Y/n) ‣ y
// Press ENTER or y

? Do you want to install this sample projects dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) ‣ y
// Press ENTER or y

The standard Hardhat project comes with a pre-created Lock.sol contract and deploy.js script. It's best to clean up your working environment before moving forward:

rm -rf ./contracts/Lock.sol ./scripts/deploy.js ./test/Lock.js

Your folder files will look like this:

⚒ Configuring XDC Mainnet and Apothem Testnet on Hardhat

In order to get started deploying new contracts on XDC Mainnet and/or Apothem, you'll need to install a new dependency called dotenv that will be used in the hardhat.config.js file:

npm install dotenv

You will need to configure a .env file with XDC Mainnet and Apothem Testnet RPC endpoints, plus the Private Key of the wallet you are using for deployment. Start by running:

touch .env

Next, write the following info in our .env file:

XINFIN_NETWORK_URL=https://erpc.xinfin.network
APOTHEM_NETWORK_URL=https://erpc.apothem.network
PRIVATE_KEY=202e3c9d30bbeca38d6578659919d4c3dc989ae18c16756690877fdc4dfa607f

🚨 Do not use the Private Key in the example above or you can risk losing your assets! 🚨

Finally, you can configure the hardhat.config.js file for both Apothem and XinFin Networks by writing:

require("@nomicfoundation/hardhat-toolbox");
require('dotenv').config();

module.exports = {
  solidity: "0.8.16",
  networks: {
    xinfin: {
      url: process.env.XINFIN_NETWORK_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
     apothem: {
      url: process.env.APOTHEM_NETWORK_URL,
      accounts: [process.env.PRIVATE_KEY]
    }
  },
};

⚒ Adding Testnet XDC to Development Wallet

You should check your Signer's Address on Hardhat by accessing the Hardhat console:

npx hardhat console --network xinfin

If you get an error that Hardhat is not installed locally, and you are using a Windows OS, you will need to execute:

npm install --save-dev @nomicfoundation/hardhat-toolbox

Once the hardhat console CLI opens, you can run:

> const hre = require("hardhat");
// Should log: Undefined
> const [owner] = await ethers.getSigners();
// Should log: Undefined
> owner.address
// Should log: '0xA4e66f4Cc17752f331eaC6A20C00756156719519' or your wallet address if you are using a different Private Key

This account is on the Ethereum standard format starting with 0x, but we can simply switch 0x for xdc. In our example, the signer wallet address is: xdcA4e66f4Cc17752f331eaC6A20C00756156719519.

With this account in hand, you can head to the Apothem Faucet and claim some TXDC for development purposes:

💵 Writing our first XRC721 Token

The source code for the XRC721 Token used in this tutorial is available here: XRC721 Contract Folder. But we will address all Events, Methods and Constants mentioned in the section 📰 About XRC721 Tokens.

Start by creating the XRC721.sol file:

touch ./contracts/XRC721.sol

You will have to use OpenZeppelin contracts, so please make sure it is installed using the following command:

npm install @openzeppelin/contracts

Next, paste this code in your XRC721 file:

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

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

contract XRC721 is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor(string memory name, string memory symbol) ERC721(name, symbol) {
    }
}

Thanks to OpenZeppelin, we don't have to implement all the code ourself. It's still a good excerize to go through the basic parts of XRC721 contract as explained below

💵 Events

As mentioned in 📰 About XRC721 Tokens, events are an important part of a smart contract logic. Events have indexed variables that can be filtered by off-chain interfaces. We might be tempted to index all the variables that are tied to an on-chain event, however Solidity has a maximum of 3 indexed variable limitation for events. XRC721 has three basic events: Transfer, Approval and ApprovalForAll.

interface XRC721 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}

💵 Methods

You must create the six methods mentioned in 📰 About XRC721 Tokens (ownerOf, balanceOf, safeTransferFrom, transferFrom, approve, setApprovalForAll, isApprovedForAll and getApproved) and a constructor. This function is only called once, when the contract is deployed, where it contains information such as the token name, decimals and/or initial token supply:

// SPDX-License-Identifier: MIT
interface XRC721 {
    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool _approved) external;

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;
}

💵 XRC165

We didn't mention it before, but XRC721 also requires implimentation of a XRC165 standard. Thanks to OpenZeppelin we don't have to implement it, but it is really simple. There is only one method, supportsInterface, and it goes as follows:

/**
 * @dev Interface of the XRC165 standard
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others (`XRC165Checker`).
 *
 * For an implementation, see `XRC165`.
 */
interface IXRC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`.
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

💵 Enabling minting

Now that you have a XRC721 contract, how can you mint an NFT with it? With the mintToken method, that's how! Each time mintToken is called, it will create new unique token assign to tokenOwner.

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

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

contract XRC721 is ERC721 {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor(string memory name, string memory symbol) ERC721(name, symbol) {
    }

    function mintToken(address tokenOwner)
        public
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(tokenOwner, newItemId);

        return newItemId;
    }
}

💵 Compiling and Deploying

You can now compile your XRC721.sol by running:

npx hardhat compile

If everything is correctly configured and there are no errors, you should see the following message on your console:

Downloading compiler 0.8.16
Compiled 1 Solidity files successfully

In order to deploy our newly compiled contract artifacts to the blockchain, we need to create a deployment script into the script folder:

touch ./scripts/deploy.js

Write the following script to the deploy.js file:

async function main() {
  const [deployer] = await ethers.getSigners();

  const XRC721 = await ethers.getContractFactory("XRC721");
  const myNFT = await XRC721.deploy("MyNFTToken", "myNFT");

  await myNFT.deployed();
  
  console.log("Token Successfully Deployed!");
  console.log("Token address:", myNFT.address);

  // and now lets mint token
  const newItemId = await myNFT.mintToken(deployer.address)

  console.log("NFT minted: ", newItemId)
}

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

If the deployment script have no errors, you can run the following command for deployment on the XDC mainnet:

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

Or this command, for deployment on the XDC Apothem Testnet:

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

In either case, you need to have enough funds to pay for gas fees on the address that is being used for development.

If the deployment is successful, the console will log the following message after migrations complete processing:

Token Successfully Deployed!
Token address: 0xbC5bA2B6e2f74EC1e8e5A310a42F65D185691Af2

Find out how your freshly minted NFT looks on Apothem Block Explorer

🔍 Veryfing Contracts on the Block Explorer

Once you have successfully deployed your smart contract to the blockchain, it might be interesting to verify you contract on XinFin Block Explorer.

Simply grab the XRC721.sol address from the previous step: this address is in the Ethereum standard but we can simply swap the 0x prefix for xdc and search for our newly deployed contract on XinFin Block Explorer:

Click in the Verify And Publish Option.

You will be redirected to the contract verification page where you will need to fill out:

  • Contract Name: XRC721

  • Compiler: Check your hardhat-config.js file for Compiler Version

  • Contract Code: Just paste everything from your XRC721.sol file

Once everything is filled out, press Submit!

If everything is correctly filled out, your contract page on the block explorer will display a new tab called Contract:

For more information about Hardhat, Please Visit Hardhat Documentation. For more information about the XDC Network, Please Visit XDC Network Documentation on GitBook. Resources used during the deployment of the XRC721 Token can be found at XRC721 Contract Folder.

Last updated