Use Hardhat with Dijets Utility Chain
Introduction#
The Utility Chain is one of Dijets Primary Blockchains. Being an instance of Ethereum Virtual Machine, its API is almost identical to an Ethereum node's API. this allows the Utility Chain to offer the same interface as Ethereum but with much higher speed, higher throughput, lower fees and lower transaction confirmation times. These properties considerably improve the performance of DApps and the user experience of smart contracts.
The goal of this guide is to lay out best practices regarding writing, testing and deployment of smart contracts on Dijets's Utility Chain. We will build the smart contracts using Hardhat development environment.
Prerequisites#
NodeJS and Yarn#
First, install the LTS (long-term support) version of
NodeJS. This is 18.x
at the time of writing. NodeJS
bundles npm
.
Next, install yarn:
_10npm install -g yarn
DijetsNodeGo and Dijets-Up#
DijetsNodeGo is a Dijets node implementation written in Go. Dijets-up is a tool to quickly deploy local test networks. Together, you can deploy local test networks and run tests on them.
Solidity and Dijets#
It is also helpful to have a basic understanding of Solidity and Dijets.
Dependencies#
Clone the Dijets smart contracts repo and
install the necessary packages via yarn
.
_10git clone https://github.com/Dijets-Inc/dijets-smart-contracts-guide.git_10cd dijets-smart-contracts-guide_10yarn
Write Contracts#
Edit the ExampleERC20.sol
contract in contracts/ExampleERC20.sol
is an
Open Zeppelin
ERC20 contract. ERC20 is a popular
smart contract interface. You can also add your own contracts.
Hardhat Config#
Hardhat uses hardhat.config.js
as the configuration file. You can define
tasks, networks, compilers and more in that file. For more information see
here.
Here is an example of a pre-configured hardhat.config.ts
file.
_56import { task } from "hardhat/config"_56import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"_56import { BigNumber } from "ethers"_56import "@nomiclabs/hardhat-waffle"_56_56export default {_56 solidity: {_56 compilers: [_56 {_56 version: "0.5.16"_56 },_56 {_56 version: "0.6.2"_56 },_56 {_56 version: "0.6.4"_56 },_56 {_56 version: "0.7.0"_56 },_56 {_56 version: "0.8.0"_56 }_56 ]_56 },_56 networks: {_56 hardhat: {_56 gasPrice: 225000000000,_56 chainId: 98200 //Only specify a chainId if we are not forking_56 },_56 local: {_56 url: 'http://localhost:9650/ext/bc/C/rpc',_56 gasPrice: 225000000000,_56 chainId: 12345,_56 accounts: [_56 "0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027",_56 "0xbbc2865b76ba28016bc2255c7504d000e046ae01934b04c694592a6276988630",_56 "0xcdbfd34f687ced8c6968854f8a99ae47712c4f4183b78dcc4a903d1bfe8cbf60",_56 "0x86f78c5416151fe3546dece84fda4b4b1e36089f2dbc48496faf3a950f16157c",_56 "0x750839e9dbbd2a0910efe40f50b2f3b2f2f59f5580bb4b83bd8c1201cf9a010a"_56 ]_56 },_56 testnet: {_56 url: 'https://testnet.dijets.io/ext/bc/C/rpc',_56 gasPrice: 225000000000,_56 chainId: 98199,_56 accounts: []_56 },_56 mainnet: {_56 url: 'https://dijets.ukwest.cloudapp.azure.com:443/ext/bc/C/rpc',_56 gasPrice: 225000000000,_56 chainId: 98200,_56 accounts: []_56 }_56 }_56}
This configures necessary network information to provide smooth interaction with Dijets. There are also some pre-defined private keys for testing on a local test network.
The port in this tutorial uses 9650. Depending on how you start your local network, it could be different. Dijets-Up can be run with flags specifying port numbers for each of your nodes.
Hardhat Tasks#
You can define custom hardhat tasks in hardhat.config.ts
.
There are two tasks included as examples: accounts
and balances
.
_16task("accounts", "Prints the list of accounts", async (args, hre): Promise<void> => {_16 const accounts: SignerWithAddress[] = await hre.ethers.getSigners()_16 accounts.forEach((account: SignerWithAddress): void => {_16 console.log(account.address)_16 })_16})_16_16task("balances", "Prints the DJT account balances for each address", async (args, hre): Promise<void> => {_16 const accounts: SignerWithAddress[] = await hre.ethers.getSigners()_16 for(const account of accounts){_16 const balance: BigNumber = await hre.ethers.provider.getBalance(_16 account.address_16 );_16 console.log(`${account.address} has balance ${balance.toString()}`);_16 }_16})
npx hardhat accounts
prints the list of accounts. npx hardhat balances
prints the list of
DJT account balances. As with other yarn
scripts you can pass in a
--network
flag to hardhat tasks.
Accounts#
Prints a list of accounts on the local network started with Dijets-up.
_10npx hardhat accounts --network local
Balances#
Prints a list of accounts and their corresponding DJT balances on the local network started with Dijets-up.
_10npx hardhat balances --network local
The first account with DJT balance is the same account we used in previous tutorials too. This address and the private key associated with it is the pre-funded account specified in the local network genesis file.
ERC20 Balances#
_10task("check-erc20-balance", "Prints out the ERC20 balance of your account").setAction(async function (taskArguments, hre) {_10 const genericErc20Abi = require("./erc20.abi.json");_10 const tokenContractAddress = "0x...";_10 const provider = ethers.getDefaultProvider("https://dijets.ukwest.cloudapp.azure.com:443/ext/bc/C/rpc");_10 const contract = new ethers.Contract(tokenContractAddress, genericErc20Abi, provider);_10 const balance = await contract.balanceOf("0x...");_10 console.log(`Balance in wei: ${balance}`)_10});
This will return the result in wei. If you want to know the exact amount of token with its token name then you need to divide it with its decimal.
Here's an example of erc20.abi.json
- genericErc20Abi
The example uses the Utility Chain Public
API for the provider. For a local
Dijets network use http://127.0.0.1:9650/ext/bc/C/rpc
and for Testnet
use https://testnet.dijets.io/ext/bc/C/rpc
.
Hardhat Help#
Run yarn hardhat
to list Hardhat's version, usage instructions, global options and available tasks.
Compile Smart Contracts#
In
package.json
there's a script named compile
for compiling smart contracts
_10"compile": "npx hardhat compile",
To compile the smart contract run:
Deploy Smart Contracts#
Hardhat enables deploying to multiple environments. You can do so by editing the
deployment script in scripts/deploy.ts
_10"deploy": "npx hardhat run scripts/deploy.ts",
You can choose which environment you want to deploy to by passing in the
--network
flag with local
(for example a local network created with Dijets-Up CLI),
testnet
, or mainnet
for each respective environment. If you
don't pass in --network
then it will default to the hardhat network. For
example, if you want to deploy to Mainnet:
To deploy the contract to Mainnet run:
To deploy the contract to your local network run:
We now have a token deployed at 0x17aB05351fC94a1a67Bf3f56DdbB941aE6
.
Interact with Smart Contract#
Hardhat has a developer console to interact with contracts and the network. For more information about Hardhat's console see here. Hardhat console is a NodeJS-REPL, and you can use different tools in it. Ethers is the library that we'll use to interact with our network.
You can open the hardhat console with:
Get the contract instance with factory and contract address to interact with our contract:
_10> const Coin = await ethers.getContractFactory('ExampleERC20');_10undefined_10> const coin = await Coin.attach('0x17aB05351fC94a1a67Bf3f56DdbB941aE6')_10undefined
The first line retrieves contract factory with ABI & bytecode. The second line
retrieves an instance of that contract factory with given contract address.
Recall that our contract was already deployed to
0x17aB05351fC94a1a67Bf3f56DdbB941aE6
in the previous step.
Fetch the accounts:
_10> let accounts = await ethers.provider.listAccounts()_10undefined_10> accounts_10[_10 '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',_10 '0x78A23300E04FB5d5D2820E23cc679738982e1fd5',_10 '0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293',_10 '0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB',_10 '0x0Fa8EA536Be85F32724D57A37758761B86416123'_10]
This is exactly the same account list as in yarn accounts
.
Now we can interact with our ERC-20
contract:
_10> let value = await coin.balanceOf(accounts[0])_10undefined_10> value.toString()_10'123456789'_10> value = await coin.balanceOf(accounts[1])_10BigNumber { _hex: '0x00', _isBigNumber: true }_10> value.toString()_10'0'
account[0]
has a balance because account[0]
is the default account. The
contract is deployed with this account. The constructor of
ERC20.sol
mints TOTAL_SUPPLY
of 123456789 token to the deployer of the contract.
accounts[1]
currently has no balance. Send some tokens to accounts[1]
, which is 0x9632a79656af553F58738B0FB750320158495942
.
_25> let result = await coin.transfer(accounts[1], 100)_25undefined_25> result_25{_25 hash: '0x35eec91011f9089ba7689479617a90baaf8590395b5c80bb209fa7000e4848a5',_25 type: 0,_25 accessList: null,_25 blockHash: null,_25 blockNumber: null,_25 transactionIndex: null,_25 confirmations: 0,_25 from: '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',_25 gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },_25 gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },_25 to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',_25 value: BigNumber { _hex: '0x00', _isBigNumber: true },_25 nonce: 3,_25 data: '0xa9059cbb0000000000000000000000009632a79656af553f58738b0fb7503201584959420000000000000000000000000000000000000000000000000000000000000064',_25 r: '0xc2b9680771c092a106eadb2887e5bff41fcda166c8e00f36ae79b196bbc53d36',_25 s: '0x355138cb5e2b9f20c15626638750775cfc9423881db374d732a8549d05ebf601',_25 v: 86260,_25 creates: null,_25 chainId: 98200,_25 wait: [Function (anonymous)]_25}
Note: Since this is a local network, we did not need to wait until transaction
is accepted. However for other networks like testnet
or mainnet
you need to
wait until transaction is accepted with: await result.wait()
.
Now we can ensure that tokens are transferred:
_10> value = await coin.balanceOf(accounts[0])_10BigNumber { _hex: '0x075bccb1', _isBigNumber: true }_10> value.toString()_10'123456689'_10> value = await coin.balanceOf(accounts[1])_10BigNumber { _hex: '0x64', _isBigNumber: true }_10> value.toString()_10'100'
As you might noticed there was no "sender" information in await coin.transfer(accounts[1], 100)
; this is because ethers
uses the first signer
as the default signer. In our case this is account[0]
. If we want to use
another account we need to connect with it first.
_10> let signer1 = await ethers.provider.getSigner(1)_10> let contractAsSigner1 = coin.connect(signer1)
Now we can call the contract with signer1
, which is account[1]
.
_23> await contractAsSigner1.transfer(accounts[0], 5)_23{_23 hash: '0x807947f1c40bb723ac312739d238b62764ae3c3387c6cdbbb6534501577382dd',_23 type: 0,_23 accessList: null,_23 blockHash: null,_23 blockNumber: null,_23 transactionIndex: null,_23 confirmations: 0,_23 from: '0x9632a79656af553F58738B0FB750320158495942',_23 gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },_23 gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },_23 to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',_23 value: BigNumber { _hex: '0x00', _isBigNumber: true },_23 nonce: 2,_23 data: '0xa9059cbb0000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fc0000000000000000000000000000000000000000000000000000000000000005',_23 r: '0xcbf126dd0b109491d037c5f3af754ef2d0d7d06149082b13d0e27e502d3adc5b',_23 s: '0x5978521804dd15674147cc6b532b8801c4d3a0e94f41f5d7ffaced14b9262504',_23 v: 86259,_23 creates: null,_23 chainId: 98200,_23 wait: [Function (anonymous)]_23}
Let's check balances now:
_10> value = await coin.balanceOf(accounts[0])_10BigNumber { _hex: '0x075bccb6', _isBigNumber: true }_10> value.toString()_10'123456694'_10> value = await coin.balanceOf(accounts[1])_10BigNumber { _hex: '0x5f', _isBigNumber: true }_10> value.toString()_10'95'
Join Dijets Public support space on Qowalts to learn more and ask any questions you may have.