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 and building the off-chain bits, so I haven't dove into Solidity dev in some time. Connext 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 and you'll be fully set up to develop, compile, test and deploy in a full-featured Typescript dev environment! Starter Kit repo Buidler (Replacement for Truffle) 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 , , , and features to bootstrap your Solidity projects. A killer feature Buidler touts is stack traces when your Solidity contracts revert 😱! Buidler init compile test migrate Ethers.js (Replacement for Web3.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. Ethers.js Waffle (Replacement for Truffle test utilities) 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. Ethereum Waffle 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 . It doesn't really matter what the values are set to for the scope of this exercise. npm init 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). 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 $ npx buidler Create a few directories to hold your project files: $ mkdir contracts scripts test Set Up Typescript Install the required Typescript dependencies: $ npm install --save-dev ts-node typescript @types/node @types/mocha Create a file in the project root: tsconfig { : { : , : , : , : , : }, : [ , ], : [ ] } "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 { BuidlerConfig } ; config: BuidlerConfig = {}; config; import from "@nomiclabs/buidler/config" const export default Creating and Compiling Contracts Now, we're ready to start writing some code! Create a very simple Solidity contract called in the directory (latest Solidity version at the time of writing was ): Counter.sol contracts/ 0.5.12 pragma solidity . . contract Counter { uint256 = event CountedTo(uint256 number) function countUp() public returns (uint256) { uint256 newCount = + require(newCount > , ) = newCount emit CountedTo( ) return } function public returns (uint256) { uint256 newCount = - require(newCount < , ) = newCount emit CountedTo( ) return } } 0 5 12 ; count 0 ; ; count 1 ; count "Uint256 overflow" ; count ; count ; count ; countDown() count 1 ; count "Uint256 underflow" ; count ; count ; count ; Set the Solidity version in (autocomplete type annotations!!): buidler.config.ts Buidler conveniently bundles a compilation task, so compiling is a piece of cake: Compiling... Compiled 1 contract successfully $ npx buidler compile 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 @nomiclabs/buidler-ethers ethers ethereum-waffle chai @types/chai 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" Set up to use the Ethers plugin: buidler.config.ts { BuidlerConfig, usePlugin } ; usePlugin( ); config: BuidlerConfig = { solc: { version: } }; config; import from "@nomiclabs/buidler/config" "@nomiclabs/buidler-ethers" const "0.5.12" export default Set Up 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! TypeChain 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! Begin by installing the library and the Ethers binding: $ npm install --save-dev typechain typechain-target-ethers Generate the type files by running the command. defines where the generated files will be stored, and the quoted string at the end will pick up any generated contracts in our directory: outDir build $ ./node_modules/.bin/typechain --target ethers --outDir typechain 'build/*.json' Now inside the directory, you should see a few files generated, one of which is . That's the main contract types file, and gives us what we need to write type safe tests! typechain/ Counter.d.ts Writing and Running Contract Tests Writing tests mostly follows the Waffle with one main difference: the object is imported from the library instead of the library. syntax ethers.provider "@nomiclabs/buidler" ethereum-waffle Now let's write a test. Create a file called inside the directory: counter.ts test/ { ethers } ; chai ; { deployContract, getWallets, solidity } ; CounterArtifact ; { Counter } chai.use(solidity); { expect } = chai; describe( , { provider = ethers.provider; [wallet] = getWallets(provider); counter: Counter; beforeEach( () => { counter = deployContract(wallet, CounterArtifact) Counter; initialCount = counter.getCount(); expect(initialCount).to.eq( ); expect(counter.address).to.properAddress; }); it( , () => { counter.countUp(); count = counter.getCount(); expect(count).to.eq( ); counter.countUp(); count = counter.getCount(); expect(count).to.eq( ); }); it( , () => { counter.countDown(); count = counter.getCount(); expect(count).to.eq( ); }); }); import from "@nomiclabs/buidler" import from "chai" import from "ethereum-waffle" import from "../build/Counter.json" import from "../typechain/Counter" const "Counter" => () // 1 const // 2 let // 3 let async await as const await // 4 0 // 5 "should count up" async await let await 1 await await 2 "should count down" async // 6 await const await 0 Explanation of numbered lines: 1. Set up using the one imported from Buidler. provider 2. Get a wallet from the function. Also note, you can destructure any number of wallets from this function, for example: getWallets [wallet1, wallet2, wallet3] = getWallets(provider); let 3. Import the Counter type and use it as the type of the variable that gets deployed in the . beforeEach 4. Waffle has some useful Chai matchers for writing contract tests like BigNumber matchers and Ethereum address matchers. Check them all out . here 5. Simple test to count up and make sure the counter works. 6. 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! $ npx buidler test Notice something unusual in the results? All contracts have already been compiled, skipping compilation. Counter ✓ should count up (143ms) 1) should count down 1 passing (593ms) 1 failing 1) Counter should count down: Error: VM Exception while processing transaction: revert Uint256 underflow at Counter.countDown (contracts/Counter.sol:24) It's 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. 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 file. We'll use rinkeby for this, but you can add any network (i.e. mainnet) similarly: buidler.config.ts { BuidlerConfig, usePlugin } ; waffleDefaultAccounts ; usePlugin( ); INFURA_API_KEY = ; RINKEBY_PRIVATE_KEY = ; config: BuidlerConfig = { solc: { version: }, paths: { artifacts: }, networks: { buidlerevm: { accounts: waffleDefaultAccounts.map( ({ balance: acc.balance, privateKey: acc.secretKey })) }, rinkeby: { url: , accounts: [RINKEBY_PRIVATE_KEY] } } }; config; import from "@nomiclabs/buidler/config" import from "ethereum-waffle/dist/config/defaultAccounts" "@nomiclabs/buidler-ethers" const "" const "" const "0.5.12" "./build" => acc `https://rinkeby.infura.io/v3/ ` ${INFURA_API_KEY} export default 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 folder called : scripts/ deploy.ts { ethers } ; { factory = ethers.getContract( ) contract = factory.deploy(); .log(contract.address); .log(contract.deployTransaction.hash); contract.deployed() } main() .then( process.exit( )) .catch( { .error(error); process.exit( ); }); import from "@nomiclabs/buidler" async ( ) function main const await "Counter" // If we had constructor arguments, they would be passed into deploy() let await // The address the Contract WILL have once mined console // The transaction that was sent to the network to deploy the Contract console // The contract is NOT deployed yet; we must wait until it is mined await => () 0 => error console 1 Super easy stuff! Now, just run the script and we can see our address and transaction hashes right in the console: All contracts have already been compiled, skipping compilation. 0x01FF454Dd078dC7f3cd0905601d093b17E7B9CD7 0x2ae1444920ed76420fb69c9f2fc914c20956efc2ae05c94ab1ea53f224aa0930 $ npx buidler run --network rinkeby scripts/deploy.ts We can go to and see that the transaction in fact completed successfully. Etherscan 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 && npm run typechain" "compile" "npx buidler compile" "typechain" "npx buidler compile && typechain --outDir typechain --target ethers 'build/*.json'" "test" "npx buidler test" The script does both contract compilation and generates TypeChain bindings, and the script runs the contract tests. build test 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" Next, we add the required configuration to our (hop over to and get an API key from your account page if you haven't yet): buidler.config.ts Etherscan { BuidlerConfig, usePlugin } ; waffleDefaultAccounts ; usePlugin( ); usePlugin( ); INFURA_API_KEY = ; RINKEBY_PRIVATE_KEY = ; ETHERSCAN_API_KEY = ; config: BuidlerConfig = { solc: { version: }, paths: { artifacts: }, networks: { buidlerevm: { accounts: waffleDefaultAccounts.map( ({ balance: acc.balance, privateKey: acc.secretKey })) }, rinkeby: { url: , accounts: [RINKEBY_PRIVATE_KEY] } }, etherscan: { url: , apiKey: ETHERSCAN_API_KEY } }; config; import from "@nomiclabs/buidler/config" import from "ethereum-waffle/dist/config/defaultAccounts" "@nomiclabs/buidler-ethers" "@nomiclabs/buidler-etherscan" const "" const "" const "" const "0.5.12" "./build" => acc `https://rinkeby.infura.io/v3/ ` ${INFURA_API_KEY} // The url for the Etherscan API you want to use. "https://api-rinkeby.etherscan.io/api" // Your API key for Etherscan // Obtain one at https://etherscan.io/ export default 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: 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 $ npx buidler verify-contract --contract-name Counter --address 0xF0E6Ea29799E85fc1A97B7b78382fd034A6d7864 Easy as pie! Now, check the contract address on , and you can see the full contract source code and read and write the contract directly from the webpage. Etherscan 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. Stay tuned for more follow-up posts regarding full-stack dapp development and tooling!