Hardhat and EthersJs
Hardhat and EthersJs tutorial - HSCS workshop. Learn how to enable custom logic & processing on Hedera through smart contracts.
Video
Hardhat and EthersJs
Hardhat is a development framework, that is designed specifically to enable smart contract development workflows. EthersJs is a software library, which enables client application development in Javascript. These two work very well together as hardhat integrates strongly with EthersJs by augmenting EthersJs with many convenience and utility functions that improve the developer experience of smart contract development.
Prerequisites
✅ Complete the Introduction section of this same tutorial.
✅ Optionally, complete the Hedera SDK JS section of this tutorial.
Set up the project
To follow along, enter the hederasdkjs
directory within the accompanying tutorial GitHub repository, which you should already have cloned in the Intro section earlier.
Then install dependencies from npm.
Step J1: Copy smart contract
We have already written the smart contract in the Intro section of this tutorial. Let's copy that into this directory so that we may continue working on it.
Hardhat REPL
A Read-Evaluate-Print Loop (REPL) is an environment which takes input from you, executes that input, and prints the result as output; then start over again. The POSIX-compliant shell that you have been using, such as bash
or zsh
, is an example of this.
Hardhat has its own REPL feature, which executes commands in the context of the smart contract project you are developing, and does so while connected to a specific EVM-compatible network.
Let's fire up the Hardhat REPL, connected to Hedera Testnet.
Note that the network name here is hederatestnet
, which is defined within the configuration file hardhat.config.js
.
You will notice that the prompt prefix changes, and is now >
. Whatever commands you enter now are no longer going to be executed by the regular shell, but instead by the Hardhat REPL. You can enter a .exit
command to exit the Hardhat REPL, and return to your regular shell at any time. Let's execute a few commands within the Hardhat REPL before we do exit.
Get block number
Hedera is a distributed ledger technology (DLT), however, it is not a blockchain. A blockchain network groups transactions together into blocks, and achieves network consensus on whether or not a block of transactions is valid, and which block (and therefore transactions) is the next one that should be added to the network. Hedera does not do that, instead it using a different consensus algorithm (Hashgraph), which achieves consensus on individual transactions, and adds them to the network as individual transactions, without a grouping of any kind.
That being said, in order to attain interoperability with EVM-compatible networks, the concept of blocks was introduced, in a manner that does not involve the consensus algorithm. In effect it simply deems all transactions successfully added to the network to be part of the same block based on their timestamp, once approximately every 2 seconds.
JSON-RPC is a Remote Procedure Call protocol, where the requests and responses are serialised in JSON. Importantly, Ethereum has defined a JSON-RPC API, and this API has become the de-facto standard API to interact with EVM-compatible networks.
Let's verify that we are able to interact with Hedera Testnet using JSON-RPC by issuing an eth_getBlockByNumber
JSON-RPC request. The expected response will be the most recent block's number on the network.
This does indeed respond with a block number, in hexadecimal.
Check block number on Hashscan
Convert this to decimal:
0x6f8741
-->7309121
Check on Hashscan: https://hashscan.io/testnet/blocks --> latest block is
7309352
--> more, as new blocks occur every 2 seconds, approximatelyClick on it: https://hashscan.io/testnet/block/7309352 --> timestamp is
3:41:10.0120 PM Jul 14, 2023
--> matches time now
Address
Let's continue with the REPL, and issue another command. This time it will not be a JSON-RPC request, but rather querying Hardhat itself to see which account we'll be using by default when performing any requests.
The accounts that Hardhat uses are generated from the seed phrase in the .env
file, plus the derivation path, using logic similar to the following code.
Note that this has already been done for you in hardhat.config.js
.
Here, hre
is a global object exported by Hardhat, and it stands for Hardhat Runtime Environment.
The hre.ethers
object exposes an instance of the EthersJs software library that has been initialised by Hardhat using the configuration from hardhat.config.js
. This includes the signers which are a list of several accounts.
This outputs an EVM address of a Hedera EVM account.
If you have completed the Hedera SDK JS section of this tutorial, you will notice that this is different from the account used there, which was 0x7394111093687e9710b7a7aeba3ba0f417c54474
. This is because the script used for the Hedera SDK JS account was configured to use the operator account. Hardhat, on the other hand, uses one of the EVM accounts generated using the BIP-39 seed phrase. These were generated during Step B4: Fund several Hedera EVM accounts in the Intro section of this tutorial.
Alright, you've completed the checks needed, verifying that you are able to successfully connected to Hedera Testnet.
Exit the REPL.
You'll now return to your regular shell.
Check address on Hashscan
Copy the EVM address that Hardhat uses by default
Go to Hashscan, and search for that address
You should get redirected to an Account page
For example, from:
https://hashscan.io/testnet/account/0x07ffAaDFe3a598b91ee08C88e5924be3EfF35796
to:
https://hashscan.io/testnet/account/0.0.3996359
This verifies that the account exists
Check that the account has a balance of HBAR
If it does not exist, or does not have balance, you'll need to create or fund it before proceeding
To do so, you'll need to repeat Step B4: Fund several Hedera EVM accounts from the Setup section of this tutorial.
Compiling smart contracts
Using hardhat to compile smart contracts
In the Hedera SDK JS section of this tutorial, in the Using solc
to compile smart contracts step, you manually installed a specific version of the Solidity compiler, and then ran that in the shell.
Hardhat takes care of that for you, it will simply read which version of the Solidity compiler has been set in in the configuration in hardhat.config.js
, and then install that particular version if necessary, and run it, and manage its outputs including caching where relevant.
Let's take a look at the compiled outputs, and where Hardhat stores them.
Build cache:
hardhat/cache/solidity-files-cache.json
ABI:
hardhat/artifacts/contracts/trogdor.sol/Trogdor.json
Bytecode + other compiler outputs:
hardhat/artifacts/build-info/${SOME_HASH}.json
You do not need to do anything with these, just good to know what is happening behind the scenes.
HAPIs and EVM transactions
However, Hardhat + EthersJs do not understand HAPIs, and are only aware of the EVM transaction model. Thankfully, Hedera supports a HAPI named EthereumTransaction
, defined in HIP-410. All the different types of EVM transactions are supported through this single HAPI.
This is the low-level protocol supported by the Hedera network which is the gateway for software libraries, developer tools, developer frameworks, and even end use software such as wallets, which were originally designed to work with Ethereum, to also work on Hedera, and is an integral part of Hedera's EVM-compatibility.
In this section of the tutorial, you are using Hardhat and EthersJs, and this framework/ software library are unaware of HAPIs, including ContractCreateTransaction
, ContractExecuteTransaction
, and ContractCallQuery
, which you used earlier.
The only API that they are able to use is JSON-RPC, and this is where the EthereumTransaction
HAPI comes into play. When you send a JSON-RPC request to an RPC endpoint for a Hedera network, that gets converted into an EthereumTransaction
HAPI. The same happens in reverse with the response.
Deploying smart contracts
Using Hardhat to deploy smart contracts
Let's begin by entering the REPL again.
Next, let's use the EthersJs APIs to deploy the smart contract.
Note that this will send an EVM deployment transaction to Hedera Testnet, using the following sequence:
ContractFactory.deployTransaction
EthersJs Javascript API --> eth_sendRawTransaction
JSON-RPC request --> EthereumTransaction
HAPI
A deployment is performed via a transaction, just like any other interaction with the network which may change the state of the network. The transaction needs to be performed by an account, in this case, since you are performing an EthereumTransaction
, the account needs to be an EVM account.
EthereumTransaction
s may only be signed using ECDSA secp256k1 keys, which Hedera EVM accounts use.
Hedera-native accounts such as the operator account, on the other hand, use EdDSA Ed25519 keys, and therefore EthereumTransaction
s may not be signed by them.
The account signing the transaction needs to pay for the cost of processing the transaction. This is a variable fee known as gas. The account that you'll use for signing is, by default, the first EVM account generated in the script from the Intro section of this tutorial, i.e. the one with the hierarchical derivation path of m/44'/60'/0'/0/0
.
Back to the Hardhat REPL, enter the following command to obtain the default signer, which is the first EVM account mentioned above.
This will output a SignerWithAddress
object, similar to this:
Next, instantiate an instance of ContractFactory
. The Hardhat-augmented version of EthersJs is aware of the directory structure, and where to find the Solidity compiler's outputs: The binary file containing the EVM bytecode, and the JSON file containing the ABI.
This will output a ContractFactory
object, similar to this:
You can observe that it has the EVM bytecode, and has parsed the ABI into an interface.
Next, deploy the smart contract. Note that this transaction includes a network request, and so expect to wait for several seconds - it will not be instantaneous like the previous commands.
This will output a Contract
object, similar to this:
There is much more information on this object, compared to the object from the previous step, because it has been deployed.
Let's examine the deployment transaction for the smart contract in a bit more detail.
This will output an object, similar to this:
The smart contract has just been deployed!
We will need the contractAddress
property, so copy that for later use.
Using Hashscan to check smart contract deployment
Copy the output smart contract account address, e.g.
0xeB9922B24D82603A543C764A3e4c9BC451FB8752
.Visit Hashscan
Paste the copied address into the search box
You should get redirected to a "Contract" page, e.g.
https://hashscan.io/testnet/contract/0.0.15546633
In it, you can see the EVM address, e.g.
0xeb9922b24d82603a543c764a3e4c9bc451fb8752
Under "Contract Bytecode", you can see "Runtime Bytecode"
Interacting with smart contacts
Using Hardhat to interact with smart contracts
While you still have the Hardhat REPL open, let's continue by interacting with the smart contract that you have just deployed.
Issue a query of the MIN_FEE
constant.
This should reply with a BigNumber
object, whose value is 100
.
Issue a similar query, this time invoking the totalBurnt
function.
This returns a value of 0
, which is to be expected, because we have yet to invoke burninate
.
That is precisely what we'll do next: Invoke burninate
. This is a payable
function, which expects the transaction to contain a minimum of 100
tinybars sent along with it. Let's sent 123
tinybars.
This will output some details about the transaction.
However, there is nothing we really need to do with this information.
Let's query both MIN_FEE
and totalBurnt
again. The value returned for MIN_FEE
is expected to be the same, since there are no functions which modify its value, and in fact it is marked as constant
. The value returned for totalBurnt
, on the other hand, is expected to increase from its previous value, since the balance of the smart contract should increase each time the burninate
function is successfully invoked.
Query MIN_FEE
.
This returns the same value of 100
, as expected.
Query totalBurnt
.
This returns a new value of 123
, as expected as well.
Let's also query the amounts
function, which was auto-generated by the Solidity compiler for the mapping of the same name. This function expects an address
parameter. Let's use the address of the same Hedera EVM account that we used to invoke the burninate
function.
This returns a value of 123
, which is expected since that was the value that was sent along with the transaction when invoking burninate
Let's repeat the above, this time using the address of a different Hedera EVM account which has not invoked the burninate
function yet.
This returns a value of 0
, which is expected since this account has not yet invoked burninate
.
Using Hashscan to check smart contract interactions
Visit the "Contract" page for your previously deployed smart contract, e.g.
https://hashscan.io/testnet/contract/0.0.15426051
Scroll down to the "Recent Contract Calls" section
If you see "REFRESH PAUSED" at the top right of this section, press the "play" button next to it to unpause (otherwise it does not load new transactions)
You should see a list of transactions, with most recent at the top
There should be a successful transaction, denoted by the absence of an exclamation mark in a red triangle, e.g.
https://hashscan.io/testnet/transaction/1689667816.410045835
Scroll down to the "Contract Result" section
You should see "Result" as
SUCCESS
You should also see "Error Message" as
None
Scroll down to the "Logs" section
You should see a single log entry (address, data, index, and topics)
The "Address" field matches that of the smart contract
The "Index" field should be
0
since there was only a single event that was emittedThe "Topics" field corresponds to the hash of the signature of the event that was emitted, e.g.
Burnination(address,uint256)
The "Data" field corresponds to the values of the event parameters, e.g.
0x0000000000000000000000007394111093687e9710b7a7aeba3ba0f417c54474000000000000000000000000000000000000000000000000000000000000007b
is0x7394111093687e9710b7a7aeba3ba0f417c54474
(your address) and0x7b
is the amount (123
when converted to decimal)
Last updated