Hardhat: networks and providers
Originally published at HackMD.
An Ethereum node can be used to read the state of the blockchain or to send transactions that modify that state. To let you do this, Ethereum clients implement a protocol called JSON-RPC. This allows applications to rely on a (somewhat) standardized set of methods that should work with any client.
Sending a JSON-RPC call
We'll use Alchemy in these examples and, if you want to reproduce them, you'll need to get an API key from them. Even better, if you have a locally synced node, you can configure it to expose the JSON-RPC interface and use that instead.
Let's say we want to get the latest block number of the Goerli network. To do that, we have to use the eth_blockNumber
JSON-RPC method, which returns the current block number. We can send this method via curl, or any other HTTP client:
$ curl -X POST -H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0", "id": "1", "method":"eth_blockNumber", "params":[]}' \
https://eth-goerli.alchemyapi.io/v2/<API_KEY>
{"jsonrpc": "2.0", "id": "1", "result": "0x8a73e2"}
This is a mouthful, but there are only a few crucial parts; the rest is just boilerplate:
- We are sending an HTTP POST request to the Alchemy endpoint.
- The body of the request is a JSON object. This object includes the name of the method we are calling,
eth_blockNumber
. It also has a list of params, which is empty in this case. - We receive a JSON object as the response, which includes a
result
field with the value we are interested in: the current block number, as a hexadecimal string.
Other actions use other JSON-RPC methods. To send a signed transaction you use eth_sendRawTransaction
, to get a transaction receipt you use eth_getTransactionReceipt
, and so on.
All of this is meant to illustrate what happens under the hood each time you interact with a node. But we don't want to do something like that for everything.
JSON-RPC networks in Hardhat
You can interact with nodes via JSON-RPC more easily using Hardhat. We'll see how to configure a network and then send calls to it.
Before starting, go to an empty directory and install Hardhat:
$ cd /path/to/some/empty/directory
$ npm install --save-dev hardhat
Now create a hardhat.config.js
file with this content:
module.exports = {
networks: {
goerli: {
url: "https://eth-goerli.alchemyapi.io/v2/<API_KEY>"
}
}
}
This is adding a network to the Hardhat configuration. To configure a network you need at least two things: a name, which in this case is goerli
, and a URL. We could've used a different name here, like testnet
, if we wanted.
After setting this up, you can start a console connected to this network by running npx hardhat console --network goerli
, or hh console --network goerli
if you have installed the Hardhat shorthand. A node.js REPL will be started, where you can send JSON-RPC calls more easily:
> await network.provider.send("eth_blockNumber", [])
'0x8a733e'
Here, network.provider
is an object that implements the EIP-1193 standard. This is an abstraction with a minimal API used to interact with a node via JSON-RPC. When you use window.ethereum
in a dapp, you are using an instance of a EIP-1193 provider exposed by the browser wallet.
The only part we care about here is that this object has a send
function that receives the name of the method and a list of arguments.
Aside: the correct way to make calls in EIP-1193 is with the request
method, but here we'll use the old send
method because it's more concise.
You can have more than one network in your configuration:
module.exports = {
networks: {
mainnet: {
url: "https://eth-mainnet.alchemyapi.io/v2/<API_KEY>"
},
goerli: {
url: "https://eth-goerli.alchemyapi.io/v2/<API_KEY>"
}
}
}
But you can only connect to one of them at a time. If we exit the console and run hh console --network mainnet
, we can make the same call as before but to the mainnet network instead:
> await network.provider.send("eth_blockNumber")
'0xc607e5'
Local development node
Using a testnet for local development is slow, and you need to get test ether from a faucet. An easier and faster alternative is to use the local development node that comes with Hardhat. This node starts an instance of the Hardhat Network, which includes features like console.log
and Solidity stack traces.
If you run hh node
in a terminal, an HTTP server with this development node will start listening for requests in http://localhost:8545
:
$ hh node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
Accounts
========
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
...
You can now connect to this network from another terminal running hh console --network localhost
and make the same call as before:
> await network.provider.send("eth_blockNumber")
'0x0'
The response is 0x0
here because this is a new network that hasn't received any transactions.
But wait, what is this localhost
network? We haven't added it to our config. The reason we can use it is that Hardhat comes with this pre-defined network, and it's pretty much equivalent to adding this:
localhost: {
url: "http://localhost:8545"
}
Since connecting to this particular address and port is so common, Hardhat includes it by default. But you can explicitly add it to your config to customize it.
So now we have an easy way to develop locally. For example, if we want to run our tests, we can start a node in one terminal and then run hh test --network localhost
in another. But this is very cumbersome. It means working with two different terminals and killing and starting the node again and again every time we want to start from scratch.
Luckily, you don't need to do that. Besides localhost
, Hardhat comes with another pre-defined network: hardhat
. This is the same network used by the development node, but instead of starting an HTTP server, it's an in-process network that is created when you run your task, and killed at the end of it. So if you run hh test --network hardhat
, the result will be pretty much equivalent to the combination of starting a new node, running hh test --network localhost
, and then killing the node, all in one command.
Besides, this is the default network, so you only need to do hh test
.
Hardhat Network
This hardhat
network is different from the other networks we have configured so far, because no HTTP server is started at all. Instead, the provider that will be exposed is connected to an in-process, ephemeral instance of the Hardhat Network. This might surprise you if you thought that you could only interact with JSON-RPC via HTTP, but the protocol is actually transport-agnostic.
The configuration options for the Hardhat Network are also different from the options for the external networks. For example, external networks have a url
configuration field, but the Hardhat network configuration doesn't. On the other hand, the Hardhat network has a blockGasLimit
option that doesn't exist for other networks.
Keep in mind that the hardhat
entry of your configuration is used to configure both the in-process network used by default, and the network started when you run hh node
.
Ethers.js
Technically, you can do anything you want just with a JSON-RPC provider, but this is still a very low-level interface. Libraries like ethers.js offer a higher-level functionality to make your life easier.
One of these interfaces is a set of providers. These are like the EIP-1193 provider we've been used so far, but with many helper methods. For example, we previously called the eth_blockNumber
method and got a hexadecimal string in response. With an ethers provider, you would do this instead:
> await anEthersProvider.getBlockNumber()
9073582
There are two significant differences here: we use a specific and more readable method to get the current block number, and we get a number in response, not a hexadecimal string.
How do we convert our provider into an ethers.js provider? The easiest way is to use ethers.js's Web3Provider
to wrap our existing provider. If you have ethers.js installed, you can start an hh console
and execute this:
> const ethers = require("ethers")
undefined
> const anEthersProvider = new ethers.providers.Web3Provider(network.provider)
undefined
> await anEthersProvider.getBlockNumber()
9073591
Even easier, you can use the @nomiclabs/hardhat-ethers
plugin. This plugin will add an ethers property to the Hardhat runtime environment, which has all the functionality from the ethers package, but that also includes some extra things, like an already initialized provider. So if you install the plugin, import it in your config, and then start a Hardhat console again, you'll be able to do this:
> await ethers.provider.getBlockNumber()
9073599
Learn more
- If you want to know more about the JSON-RPC interface of Ethereum nodes, you can check the ethereum.org page. There is work in progress to have an up to date specification in the eth1.0-apis repository, which is used to generate this site. Finally, this information used to live in the Ethereum Wiki and, while this page isn't maintained anymore, it still has useful information.
- To learn more about the Hardhat Network, check its docs.