The New Solidity Dev Stack: Hardhat + Ethers + Waffle + Typescript [Tutorial]

Rahul Sethuram
10 min readFeb 8, 2020

--

UPDATED for Hardhat (30 OCT 2020)

UPDATED with TypeChain v2 and latest Buidler (23 MAY 2020)

Note: Originally published on HackerNoon

Ethereum development, while still very nascent in feel, has come a long way. When I started developing Solidity smart contracts and Ethereum dapps in 2017, Truffle and Web3.js were the industry standard. These are great tools and I have tons of respect for the people that built them. However, anyone who has used them has dealt with bugs and sometimes poor developer experience. There are a few new tools out there that have clearly been inspired by these first sets of tools and made the developer process much better.

I’ve been very focused on the Layer 2 side of things at Connext and building the off-chain bits, so I haven’t dove into Solidity dev in some time.

I recently participated in a hackathon where I got to dive back in and see the current state of things. I found a lot of cool tools, but not much in the way of docs on how to get things working together. I decided to write up my findings and create a project that can be a starting point for anyone that wants to build and test smart contracts and dapps.

TL;DR

Clone the Starter Kit repo and you’ll be fully set up to develop, compile, test and deploy in a full-featured Typescript dev environment!

Hardhat (Replacement for Truffle)

Hardhat bills itself as a “task runner for Ethereum smart contract developers”. In practice, this means that the tool will help you bootstrap your Solidity project with a template and give you all the scaffolding needed to test out your smart contracts and ultimately deploy onto the Ethereum blockchain. Previously, it was standard procedure to use Truffle’s init, compile, test, and migrate features to bootstrap your Solidity projects. Some killer features Hardhat touts are stack traces when your Solidity contracts revert and console.log() for debugging 😱!

Ethers.js (Replacement for Web3.js)

Ethers.js is a Javascript SDK for interacting with the Ethereum blockchain. I used Web3.js exclusively for a long time when I started Solidity development. When I tried Ethers for the first time, I was blown away by how easy it was to get set up and how nice the API is. I urge anyone who is used to working with Web3.js to give Ethers a try. It has all the necessary functions for working with wallets, accounts, and contracts, and it also has some neat utilities such as ABICoder, HDNode, BigNumber, and various formatting utilities for hex strings, ether units, and Etherum addresses.

Waffle (Replacement for Truffle test utilities)

Ethereum Waffle is a lightweight test runner for Ethereum smart contracts. It has some really nice testing utils built in like Chai matchers for Ethereum addresses, hashes, and BigNumbers, it’s Typescript native, and plays really nicely with Ethers.

Typescript Everywhere!

Typescript has been all the rage lately, and for good reason. For me, the absolute gamechanger with Typescript is the IDE integration which gives you autocomplete for all class properties, object keys, function parameters, etc. I can’t ever go back to coding vanilla Javascript after familiarizing with Typescript.

The nice thing about all the tools I mentioned above is that they all work extremely well with Typescript, and once everything is set up, developer experience is a dream.

Project Setup

Now onto the fun stuff! In an empty folder, create an npm project by running npm init. It doesn’t really matter what the values are set to for the scope of this exercise.

Install Hardhat:

$ npm install --save-dev hardhat

Bootstrap Hardhat project:

$ npx hardhat

Select the option to “Create an empty hardhat.config.js” (we will be using a different stack than the example, so we will create our own).

$ npx hardhat
888 888 888 888 888
888 888 888 888 888
888 888 888 888 888
8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
888 888 "88b 888P" d88" 888 888 "88b "88b 888
888 888 .d888888 888 888 888 888 888 .d888888 888
888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
👷 Welcome to Hardhat v2.0.2 👷‍? What do you want to do? …
❯ Create a sample project
Create an empty hardhat.config.js
Quit

Create a few directories to hold your project files:

$ mkdir contracts test scripts

Set Up Typescript

Install the required Typescript dependencies:

$ npm install --save-dev ts-node typescript @types/node @types/mocha

Create a tsconfig file in the project root:

{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "dist"
},
"include": ["./scripts", "./test"],
"files": [
"./hardhat.config.ts"
]
}

Rename the Hardhat config file and make it typesafe:

mv hardhat.config.js hardhat.config.ts

// hardhat.config.tsimport { HardhatUserConfig } from "hardhat/types";const config: HardhatUserConfig = {};
export default config;

Creating and Compiling Contracts

Now, we’re ready to start writing some code!

Create a very simple Solidity contract called Counter.sol in the contracts/ directory (latest Solidity version at the time of writing was 0.6.8):

pragma solidity ^0.6.8;import "hardhat/console.sol";contract Counter {
uint256 count = 0;
event CountedTo(uint256 number); function getCount() public view returns (uint256) {
return count;
}
function countUp() public returns (uint256) {
console.log("countUp: count =", count);
uint256 newCount = count + 1;
require(newCount > count, "Uint256 overflow");
count = newCount; emit CountedTo(count);
return count;
}
function countDown() public returns (uint256) {
console.log("countDown: count =", count);
uint256 newCount = count - 1;
require(newCount < count, "Uint256 underflow");
count = newCount; emit CountedTo(count);
return count;
}
}

Set the Solidity version in hardhat.config.ts by changing the solidity.compilers.$.version key.

Hardhat conveniently bundles a compilation task, so compiling is a piece of cake:

$ npx buidler compile
Compiling...
Compiled 1 contract successfully

The Solidity versioning system that Hardhat uses is AMAZING. Switching versions is a piece of cake, and Hardhat automatically downloads and installs Solidity versions as needed, all you need to do is change it in the config. Huge props to the Hardhat team for setting this up! You can even easily compile different Solidity versions in the same project just by making the solidity key in the config an array!

Set Up Test Environment with Ethers and Waffle

Now, we will set up our testing environment.

Install Ethers, Waffle, and the Hardhat plugin:

$ npm install --save-dev ethers @nomiclabs/hardhat-waffle ethereum-waffle

Add the required type definitions to your tsconfig.json:

{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"resolveJsonModule": true
},
"include": [
"./scripts",
"./test"
],
"files": ["./hardhat.config.ts"]
}

Set up hardhat.config.ts to use the Waffle plugin:

import { HardhatUserConfig } from "hardhat/types";import "@nomiclabs/hardhat-waffle";const config: HardhatUserConfig = {
solidity: {
compilers: [{ version: "0.6.8", settings: {} }],
},
};
export default config;

Set Up TypeChain

TypeChain is a really cool tool that gives you a full typed interface for your smart contracts. Once it’s set up we can get type hints for contract functions in Typescript!

As of this writing, Hardhat does not have a TypeChain plugin. I’m planning to build one myself soon if someone doesn’t do it first!

EDIT: Plugin has been built, the following instructions are updated to use the plugin!

EDIT2: Plugin has been updated to be zero config! Just install and it automatically runs on compile!

Begin by installing the Typechain plugin:

$ npm install --save-dev hardhat-typechain typechain ts-generator @typechain/ethers-v5

Import the plugin:

import { HardhatUserConfig } from "hardhat/types";import "@nomiclabs/hardhat-waffle";
import "hardhat-typechain";
const config: BuidlerConfig = {
solidity: {
compilers: [{ version: "0.6.8", settings: {} }],
},
};
export default config;

Simply compile the contracts to generate the Typechain artifacts! npx hardhat compile.

Now inside the typechain/ directory, you should see a few files generated, one of which is Counter.d.ts. That’s the main contract types file, and gives us what we need to write type safe tests!

Writing and Running Contract Tests

Writing tests mostly follows the Waffle syntax with one main difference: the ethers.provider object is imported from the hardhat library instead of the ethereum-waffle library.

Now let’s write a test. Create a file called counter.ts inside the test/ directory:

import { ethers } from "hardhat";
import chai from "chai";
import { solidity } from "ethereum-waffle";
import { Counter } from "../typechain/Counter";
chai.use(solidity);
const { expect } = chai;
describe("Counter", () => {
let counter: Counter;
beforeEach(async () => {
// 1
const signers = await ethers.getSigners();
// 2
const counterFactory = await ethers.getContractFactory(
"Counter",
signers[0]
);
counter = (await counterFactory.deploy()) as Counter;
await counter.deployed();
const initialCount = await counter.getCount();
// 3
expect(initialCount).to.eq(0);
expect(counter.address).to.properAddress;
});
// 4
describe("count up", async () => {
it("should count up", async () => {
await counter.countUp();
let count = await counter.getCount();
expect(count).to.eq(1);
});
});
describe("count down", async () => {
// 5
it("should fail", async () => {
// this test will fail
await counter.countDown();
});
it("should count down", async () => {
await counter.countUp();
await counter.countDown();
const count = await counter.getCount();
expect(count).to.eq(0);
});
});
});

Explanation of numbered lines:

  1. Get an array of pre-funded signers from Ethers.
  2. Deploy the contracts using the pre-funded signer. Import the Counter type and use it as the type of the variable that gets deployed in the beforeEach.
  3. Waffle has some useful Chai matchers for writing contract tests like BigNumber matchers and Ethereum address matchers. Check them all out here.
  4. Simple test to count up and make sure the counter works.
  5. Those of you that are paying attention will see that this test will fail. Wait on this to see the real magic of Hardhat.

Let’s run the tests!

First, let’s configure Hardhat to use their hardhat network which provides all the Solidity debugging magic:

import { HardhatUserConfig } from "hardhat/types";import "@nomiclabs/hardhat-waffle";
import "hardhat-typechain";
const config: HardhatUserConfig = {
defaultNetwork: "hardhat",
solidity: {
compilers: [{ version: "0.6.8", settings: {} }],
},
};
export default config;

Now, run the tests:

$ npx hardhat test

Notice something unusual in the results?

2 passing (1s)
1 failing
1) Counter
count down
should fail:
Error: VM Exception while processing transaction: revert Uint256 underflow
at Counter.countDown (contracts/Counter.sol:29)

It’s console.log OUTPUT and a STACK TRACE from your Solidity code showing the LINE NUMBER that the revert happened on!!! 😱👻💀

Gone are the days of commenting out contracts line by line to see which revert is triggered and guessing variable values.

Deploying Contracts

After testing, the final step in the cycle is to deploy your contracts.

The first step is to add a network config to your hardhat.config.ts file. We’ll use rinkeby for this, but you can add any network (i.e. mainnet) similarly:

import { HardhatUserConfig } from "hardhat/types";import "@nomiclabs/hardhat-waffle";
import "hardhat-typechain";
const config: HardhatUserConfig = {
defaultNetwork: "hardhat",
solidity: {
compilers: [{ version: "0.6.8", settings: {} }],
},
networks: {
hardhat: {},
rinkeby: {
url: `https://rinkeby.infura.io/v3/${INFURA_API_KEY}`,
accounts: [RINKEBY_PRIVATE_KEY],
},
},
};
export default config;

I’m using Infura as my Ethereum node endpoint, but any remote endpoint would work. If you haven’t done this ever, grab an API key from Infura.

Now, we create a deploy script inside our scripts/ folder called deploy.ts:

import { ethers } from "hardhat";async function main() {
const factory = await ethers.getContract("Counter");
// If we had constructor arguments, they would be passed into deploy()
let contract = await factory.deploy();
// The address the Contract WILL have once mined
console.log(contract.address);
// The transaction that was sent to the network to deploy the Contract
console.log(contract.deployTransaction.hash);
// The contract is NOT deployed yet; we must wait until it is mined
await contract.deployed();
}
main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});

Super easy stuff!

Now, just run the script and we can see our address and transaction hashes right in the console:

$ npx hardhat run --network rinkeby scripts/deploy.tsAll contracts have already been compiled, skipping compilation.
0x01FF454Dd078dC7f3cd0905601d093b17E7B9CD7
0x2ae1444920ed76420fb69c9f2fc914c20956efc2ae05c94ab1ea53f224aa0930

We can go to Etherscan and see that the transaction in fact completed successfully.

There you have it! A full step-by-step guide to setting up a supercharged build, test, deploy environment that’s typesafe and makes use of some cool new tools.

Wrapping Up

To keep everything clean and awesome, let’s make some handy NPM scripts. Add the following to your package.json:

"scripts": {
"build": "npm run compile",
"compile": "npx buidler compile",
"test": "npx buidler test"
}

The build script does both contract compilation and generates TypeChain bindings, and the test script runs the contract tests.

Bonus: Verify On Etherscan

Hardhat has a super handy plugin for verifying contracts on Etherscan, which is a task that is more complicated than it seems like it should be. Their tool handles flattening for you, which is very handy for contracts that import other contracts, make use of OpenZeppelin libs, etc.

We can start by installing the plugin:

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

Next, we add the required configuration to our hardhat.config.ts (hop over to Etherscan and get an API key from your account page if you haven’t yet):

import { HardhatUserConfig } from "hardhat/types";import "@nomiclabs/hardhat-waffle";
import "hardhat-typechain";
const config: HardhatUserConfig = {
defaultNetwork: "hardhat",
solidity: {
compilers: [{ version: "0.6.8", settings: {} }],
},
networks: {
hardhat: {},
rinkeby: {
url: `https://rinkeby.infura.io/v3/${INFURA_API_KEY}`,
accounts: [RINKEBY_PRIVATE_KEY],
},
},
etherscan: {
// Your API key for Etherscan
// Obtain one at https://etherscan.io/
apiKey: ETHERSCAN_API_KEY,
},
};
export default config;

Hopefully we kept our deployed address from the previous step handy, because then we can simply run the built in task that this plugin provides:

$ npx hardhat verify-contract --contract-name Counter --address 0xF0E6Ea29799E85fc1A97B7b78382fd034A6d7864$ npx buidler verify-contract --contract-name Counter --address 0xF0E6Ea29799E85fc1A97B7b78382fd034A6d7864All contracts have already been compiled, skipping compilation.
Successfully submitted contract at 0xF0E6Ea29799E85fc1A97B7b78382fd034A6d7864 for verification on etherscan. Waiting for verification result...
Successfully verified contract on etherscan

Easy as pie! Now, check the contract address on Etherscan, and you can see the full contract source code and read and write the contract directly from the webpage.

Final Thoughts

Hardhat seriously impressed me with its devex throughout my whole time using it. It has a ton of cool features already and they have plans to build a whole bunch more cool stuff. In addition to Solidity stack traces, the team plans to roll out another much needed smart contract debugging feature: console.log! EDIT: This has been done!

I will definitely be following this project closely and contributing to its ecosystem however I can.

If you need any help doing any of this the Discord is a great resource to get unstuck quickly. The Nomic Labs team is always hanging out there and very responsive.

Stay tuned for more follow-up posts regarding full-stack dapp development and tooling!

--

--

Rahul Sethuram

CTO @ConnextProject. Full-stack web + blockchain developer. Interested in cutting-edge tech, investing, sports, and fitness. BJJ enthusiast.