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!

Buidler (Replacement for Truffle)

Buidler 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 Buidler 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 Buidler:

$ npm install --save-dev @nomiclabs/buidler

Bootstrap Buidler project:

$ npx buidler

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

$ npx buidler

888 d8b 888 888

888 Y8P 888 888

888 888 888

88888b. 888 888 888 .d88888 888 .d88b. 888d888

888 "88b 888 888 888 d88" 888 888 d8P Y8b 888P"

888 888 888 888 888 888 888 888 88888888 888

888 d88P Y88b 888 888 Y88b 888 888 Y8b. 888

88888P" "Y88888 888 "Y88888 888 "Y8888 888

👷 Welcome to Buidler v1.0.1 👷‍‍

? What do you want to do? …

Create a sample project

❯ Create an empty buidler.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": [

"./buidler.config.ts"

]

}

Rename the Buidler config file and make it typesafe:

mv buidler.config.js buidler.config.ts

// buidler.config.ts

const config: BuidlerConfig = {};

export default config; import { BuidlerConfig } from " @nomiclabs/buidler /config";const config: BuidlerConfig = {};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 " @nomiclabs/buidler /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 buidler.config.ts by changing the solc.version key.

Buidler 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 Buidler uses is AMAZING. Switching versions is a piece of cake, and Buidler automatically downloads and installs Solidity versions as needed, all you need to do is change it in the config. Huge props to the Buidler team for setting this up!

Set Up Test Environment with Ethers and Waffle

Now, we will set up our testing environment.

Install Ethers, Waffle, and the Buidler plugin:

$ npm install --save-dev ethers @nomiclabs/buidler-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": [

"./buidler.config.ts",

"node_modules/@nomiclabs/buidler-ethers/src/type-extensions.d.ts",

"node_modules/@nomiclabs/buidler-waffle/src/type-extensions.d.ts"

]

}

Set up buidler.config.ts to use the Ethers plugin (included with the buidler-waffle plugin):

import { BuidlerConfig, usePlugin } from " @nomiclabs/buidler /config"; usePlugin(" @nomiclabs/buidler-waffle "); const config: BuidlerConfig = {

solc: {

version: "0.6.2"

}

};

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, Buidler 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!

Begin by installing the Typechain plugin:

$ npm install --save-dev buidler-typechain typechain ts-generator @typechain/ethers-v4 @typechain/truffle-v5 @typechain/web3-v1

Add the typechain section to your Buidler config to configure the plugin:

import { BuidlerConfig, usePlugin } from " @nomiclabs/buidler /config";

usePlugin("buidler-typechain"); usePlugin(" @nomiclabs/buidler-waffle ");usePlugin("buidler-typechain"); const config: BuidlerConfig = {

solc: {

version: "0.6.8"

},

typechain: {

outDir: "typechain",

target: "ethers-v4"

}

};

export default config;

outDir defines where the generated files will be stored, and the target we want to generate files for is ethers .

Add the types to your tsconfig :

{

"compilerOptions": {

"target": "es5",

"module": "commonjs",

"strict": true,

"esModuleInterop": true,

"outDir": "dist",

"resolveJsonModule": true

},

"include": ["./scripts", "./test"],

"files": [

"./buidler.config.ts",

"node_modules/@nomiclabs/buidler-ethers/src/type-extensions.d.ts",

"node_modules/@nomiclabs/buidler-waffle/src/type-extensions.d.ts",

"node_modules/buidler-typechain/src/type-extensions.d.ts"

]

}

If you run npx buidler you’ll see a new available command in the menu called typechain.

Generate the type files by running the command npx buidler typechain .

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”@nomiclabs/buidler” library instead of the ethereum-waffle library.

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

Explanation of numbered lines:

Get an array of pre-funded signers from Ethers. 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. Waffle has some useful Chai matchers for writing contract tests like BigNumber matchers and Ethereum address matchers. Check them all out here. Simple test to count up and make sure the counter works. Those of you that are paying attention will see that this test will fail. Wait on this to see the real magic of Buidler.

Let’s run the tests!

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

import { BuidlerConfig, usePlugin } from " @nomiclabs/buidler /config";

usePlugin("buidler-typechain"); usePlugin(" @nomiclabs/buidler-waffle ");usePlugin("buidler-typechain"); const config: BuidlerConfig = {

defaultNetwork: "buidlerevm",

solc: {

version: "0.6.8"

},

typechain: {

outDir: "typechain",

target: "ethers"

}

}; export default config;

Now, run the tests:

$ npx buidler 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 buidler.config.ts file. We’ll use rinkeby for this, but you can add any network (i.e. mainnet) similarly:

import { BuidlerConfig, usePlugin } from " @nomiclabs/buidler /config";

usePlugin("

usePlugin("buidler-typechain"); usePlugin(" @nomiclabs/buidler-waffle ");usePlugin(" @nomiclabs/buidler-etherscan ");usePlugin("buidler-typechain"); const INFURA_API_KEY = "";

const RINKEBY_PRIVATE_KEY = "";

defaultNetwork: "buidlerevm",

solc: {

version: "0.6.8"

},

networks: {

rinkeby: {

url: `

accounts: [RINKEBY_PRIVATE_KEY]

}

},

typechain: {

outDir: "typechain",

target: "ethers-v4"

}

}; const config: BuidlerConfig = {defaultNetwork: "buidlerevm",solc: {version: "0.6.8"},networks: {rinkeby: {url: ` https://rinkeby.infura.io/v3/${INFURA_API_KEY}` accounts: [RINKEBY_PRIVATE_KEY]},typechain: {outDir: "typechain",target: "ethers-v4"}; 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 " @nomiclabs/buidler "; 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 buidler run --network rinkeby scripts/deploy.ts All 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 && npx buidler typechain",

"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

Buidler 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/buidler-etherscan

Then, we add to our tsconfig.json to make sure our Typescript environment knows about this plugin:

{

"compilerOptions": {

"target": "es5",

"module": "commonjs",

"strict": true,

"esModuleInterop": true,

"outDir": "dist",

"resolveJsonModule": true

},

"include": ["./scripts", "./test"],

"files": [

"./buidler.config.ts",

"node_modules/@nomiclabs/buidler-ethers/src/type-extensions.d.ts",

"node_modules/@nomiclabs/buidler-etherscan/src/type-extensions.d.ts",

"node_modules/@nomiclabs/buidler-waffle/src/type-extensions.d.ts",

"node_modules/buidler-typechain/src/type-extensions.d.ts"

]

}

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

import { BuidlerConfig, usePlugin } from " @nomiclabs/buidler /config";

usePlugin("

usePlugin("buidler-typechain"); usePlugin(" @nomiclabs/buidler-waffle ");usePlugin(" @nomiclabs/buidler-etherscan ");usePlugin("buidler-typechain"); const INFURA_API_KEY = "";

const RINKEBY_PRIVATE_KEY = "";

const ETHERSCAN_API_KEY = "";

defaultNetwork: "buidlerevm",

solc: {

version: "0.6.8"

},

networks: {

rinkeby: {

url: `

accounts: [RINKEBY_PRIVATE_KEY]

}

},

etherscan: {

// The url for the Etherscan API you want to use.

url: "

// Your API key for Etherscan

// Obtain one at

apiKey: ETHERSCAN_API_KEY

},

typechain: {

outDir: "typechain",

target: "ethers"

}

}; const config: BuidlerConfig = {defaultNetwork: "buidlerevm",solc: {version: "0.6.8"},networks: {rinkeby: {url: ` https://rinkeby.infura.io/v3/${INFURA_API_KEY}` accounts: [RINKEBY_PRIVATE_KEY]},etherscan: {// The url for the Etherscan API you want to use.url: " https://api-rinkeby.etherscan.io/api ",// Your API key for Etherscan// Obtain one at https://etherscan.io/ apiKey: ETHERSCAN_API_KEY},typechain: {outDir: "typechain",target: "ethers"}; 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 buidler verify-contract --contract-name Counter --address 0xF0E6Ea29799E85fc1A97B7b78382fd034A6d7864$ npx buidler verify-contract --contract-name Counter --address 0xF0E6Ea29799E85fc1A97B7b78382fd034A6d7864 All 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

Buidler 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!

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 Buidler Support group on Telegram 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!