Using Hardhat

In this tutorial, we provide guidelines on how to create, compile, and deploy a simple Hello World smart contract on MELD using Hardhat.

Hardhat is a development environment for Ethereum software. It consists of different components for editing, compiling, debugging and deploying your smart contracts and dApps, all of which work together to create a complete development environment.

This guide will take you through the installation of our recommended setup, but as most of Hardhat's functionality comes from plugins, you are free to customize it or choose a completely different path.

Installation Process

To install hardhat, you need to create an npm project by

  1. opening your terminal

  2. creating an empty folder using the following commands

mkdir your_folder_name && cd your_folder_name
  1. running npm initand following its instructions.

You can use another package manager, like yarn, but we recommend you use npm 7 or later, as it makes installing Hardhat plugins simpler.

Once your project is ready, you should run

npm install --save-dev hardhat

If you've followed all instructions properly, you'll see something like this at this point 👇

We will explore the basics of creating a Hardhat project with a sample contract, tests of that contract, and a script to deploy it.

To create the sample project, run npx hardhat init in your project folder:

Let’s create the JavaScript or TypeScript project and go through these steps to compile, test and deploy the sample contract. We recommend using TypeScript, but if you are not familiar with it just pick JavaScript.

If you are getting warnings about using a version of Node.js that is not supported by Hardhat, you can switch versions by using

nvm install 18
// or 
nvm use 18

You can replace 18 with the version you'd like to use. 18 is the stable version at the time of writing.

Don't have nvm? You can install run this command in your terminal

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

Still stuck? Follow this guide here.

Running Tasks

To first get a quick sense of what's available and what's going on, run npx hardhat in your project folder:

$ npx hardhat
Hardhat version 2.9.9

Usage: hardhat [GLOBAL OPTIONS] <TASK> [TASK OPTIONS]

GLOBAL OPTIONS:

  --config              A Hardhat config file.
  --emoji               Use emoji in messages.
  --help                Shows this message, or a task's help if its name is provided
  --max-memory          The maximum amount of memory that Hardhat can use.
  --network             The network to connect to.
  --show-stack-traces   Show stack traces.
  --tsconfig            A TypeScript config file.
  --verbose             Enables Hardhat verbose logging
  --version             Shows hardhat's version.


AVAILABLE TASKS:

  check                 Check whatever you need
  clean                 Clears the cache and deletes all artifacts
  compile               Compiles the entire project, building all artifacts
  console               Opens a hardhat console
  coverage              Generates a code coverage report for tests
  flatten               Flattens and prints contracts and their dependencies
  help                  Prints this message
  node                  Starts a JSON-RPC server on top of Hardhat Network
  run                   Runs a user-defined script after compiling the project
  test                  Runs mocha tests
  typechain             Generate Typechain typings for compiled contracts
  verify                Verifies contract on Etherscan

To get help for a specific task run: npx hardhat help [task]

The list of available tasks includes the built-in ones and also those that came with any installed plugins. npx hardhat is your starting point to find out what tasks are available to run.

Compiling Your Contract

Next, if you take a look in the contracts/ folder, you'll see Lock.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

contract Lock {
    uint public unlockTime;
    address payable public owner;

    event Withdrawal(uint amount, uint when);

    constructor(uint _unlockTime) payable {
        require(
            block.timestamp < _unlockTime,
            "Unlock time should be in the future"
        );

        unlockTime = _unlockTime;
        owner = payable(msg.sender);
    }

    function withdraw() public {
        // Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
        // console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);

        require(block.timestamp >= unlockTime, "You can't withdraw yet");
        require(msg.sender == owner, "You aren't the owner");

        emit Withdrawal(address(this).balance, block.timestamp);

        owner.transfer(address(this).balance);
    }
}

To compile it, simply run:

npx hardhat compile

If you created a TypeScript project, this task will also generate TypeScript bindings using TypeChain.

Testing your contracts

Your project comes with tests that use Mocha, Chai, and Ethers.js.

If you take a look in the test/ folder, you'll see a test file:

TypeScriptJavaScript

import {
  time,
  loadFixture,
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";

describe("Lock", function () {
  // We define a fixture to reuse the same setup in every test.
  // We use loadFixture to run this setup once, snapshot that state,
  // and reset Hardhat Network to that snapshot in every test.
  async function deployOneYearLockFixture() {
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const ONE_GWEI = 1_000_000_000;

    const lockedAmount = ONE_GWEI;
    const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;

    // Contracts are deployed using the first signer/account by default
    const [owner, otherAccount] = await ethers.getSigners();

    const Lock = await ethers.getContractFactory("Lock");
    const lock = await Lock.deploy(unlockTime, { value: lockedAmount });

    return { lock, unlockTime, lockedAmount, owner, otherAccount };
  }

  describe("Deployment", function () {
    it("Should set the right unlockTime", async function () {
      const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);

      expect(await lock.unlockTime()).to.equal(unlockTime);
    });

    it("Should set the right owner", async function () {
      const { lock, owner } = await loadFixture(deployOneYearLockFixture);

      expect(await lock.owner()).to.equal(owner.address);
    });

    it("Should receive and store the funds to lock", async function () {
      const { lock, lockedAmount } = await loadFixture(
        deployOneYearLockFixture
      );

      expect(await ethers.provider.getBalance(lock.target)).to.equal(
        lockedAmount
      );
    });

    it("Should fail if the unlockTime is not in the future", async function () {
      // We don't use the fixture here because we want a different deployment
      const latestTime = await time.latest();
      const Lock = await ethers.getContractFactory("Lock");
      await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith(
        "Unlock time should be in the future"
      );
    });
  });

  describe("Withdrawals", function () {
    describe("Validations", function () {
      it("Should revert with the right error if called too soon", async function () {
        const { lock } = await loadFixture(deployOneYearLockFixture);

        await expect(lock.withdraw()).to.be.revertedWith(
          "You can't withdraw yet"
        );
      });

      it("Should revert with the right error if called from another account", async function () {
        const { lock, unlockTime, otherAccount } = await loadFixture(
          deployOneYearLockFixture
        );

        // We can increase the time in Hardhat Network
        await time.increaseTo(unlockTime);

        // We use lock.connect() to send a transaction from another account
        await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
          "You aren't the owner"
        );
      });

      it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
        const { lock, unlockTime } = await loadFixture(
          deployOneYearLockFixture
        );

        // Transactions are sent using the first signer by default
        await time.increaseTo(unlockTime);

        await expect(lock.withdraw()).not.to.be.reverted;
      });
    });

    describe("Events", function () {
      it("Should emit an event on withdrawals", async function () {
        const { lock, unlockTime, lockedAmount } = await loadFixture(
          deployOneYearLockFixture
        );

        await time.increaseTo(unlockTime);

        await expect(lock.withdraw())
          .to.emit(lock, "Withdrawal")
          .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg
      });
    });

    describe("Transfers", function () {
      it("Should transfer the funds to the owner", async function () {
        const { lock, unlockTime, lockedAmount, owner } = await loadFixture(
          deployOneYearLockFixture
        );

        await time.increaseTo(unlockTime);

        await expect(lock.withdraw()).to.changeEtherBalances(
          [owner, lock],
          [lockedAmount, -lockedAmount]
        );
      });
    });
  });
});

You can run your tests with npx hardhat test:

$ npx hardhat test
Generating typings for: 2 artifacts in dir: typechain-types for target: ethers-v6
Successfully generated 6 typings!
Compiled 2 Solidity files successfully


  Lock
    Deployment
      ✔ Should set the right unlockTime (610ms)
      ✔ Should set the right owner
      ✔ Should receive and store the funds to lock
      ✔ Should fail if the unlockTime is not in the future
    Withdrawals
      Validations
        ✔ Should revert with the right error if called too soon
        ✔ Should revert with the right error if called from another account
        ✔ Shouldn't fail if the unlockTime has arrived and the owner calls it
      Events
        ✔ Should emit an event on withdrawals
      Transfers
        ✔ Should transfer the funds to the owner


  9 passing (790ms)

Deploying your contracts to the MELD Blockchain

Next, to deploy the contract we will use a Hardhat script.

Inside the scripts/ folder you will find a file with the following code:

import { ethers } from "hardhat";

async function main() {
  const currentTimestampInSeconds = Math.round(Date.now() / 1000);
  const unlockTime = currentTimestampInSeconds + 60;

  const lockedAmount = ethers.parseEther("0.001");

  const lock = await ethers.deployContract("Lock", [unlockTime], {
    value: lockedAmount,
  });

  await lock.waitForDeployment();

  console.log(
    `Lock with ${ethers.formatEther(
      lockedAmount
    )}ETH and unlock timestamp ${unlockTime} deployed to ${lock.target}`
  );
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

This script will be used to deploy the Lock smart contract to the MELD Blockchain.

But before we can do this, it's necessary to configure your private key within the Hardhat environment. This key should correspond to the account you'll be using for deployment. Follow these steps to securely add your private key:

  1. Environment File: Create a .env file in the root directory of your Hardhat project if it doesn't exist already. This file should not be committed to your version control system (like Git) to keep it secure (see step 3).

  2. Store Private Key: In the .env file, add your private key as follows:

    PRIVATE_KEY='your-private-key-here'

    Replace 'your-private-key-here' with your actual private key.

  3. Git Ignore: Add the .env file to your .gitignore file to prevent accidentally pushing it to a public repository. It will look something like this

// THIS IS HOW YOUR .env FILE SHOULD LOOK LIKE

node_modules
.env

# Hardhat files
/cache
/artifacts

# TypeChain files
/typechain
/typechain-types

# solidity-coverage files
/coverage
/coverage.json

Security Disclaimers

  • Never Share Your Private Key: Your private key is akin to the key to your house. Sharing it can lead to unauthorized access to your blockchain assets and identities.

  • Version Control Safety: Never commit files containing private keys or sensitive information to version control systems, especially if the repository is public.

  • Backup Your Keys: Store a backup of your private keys in a secure location. Losing access to your keys could result in the irreversible loss of your assets.

Now navigate to the hardhat config in your hardhat.config.ts/js file and add the Kanazawa Testnet details. The updated config should look something like this:

const config: HardhatUserConfig = {
    solidity: "0.8.19",
    networks: {
        kanazawa: {
            chainId: 222000222,
            url: "https://testnet-rpc.meld.com/",
            accounts: [process.env.PRIVATE_KEY],
        },
    },
};

The accounts field under the kanazawa network is set to use process.env.PRIVATE_KEY. This tells Hardhat to use the private key from your .env file for transactions.

You're now ready to deploy your smart contract!

Go to your terminal and run:

npx hardhat run scripts/deploy.ts --network kanazawa

After a few seconds, you will see the contract address show up in the console and you can view the contract address by visiting

https://testnet.meldscan.io/address/{YOUR_CONTRACT_ADDRESS}

Congrats! You have created a project and compiled, tested and deployed a smart contract. 🔥 🎉

Last updated