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:
npm 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
.
git clone https://github.com/Dijets-Inc/dijets-smart-contracts-guide.git cd dijets-smart-contracts-guide yarn
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.
import { task } from "hardhat/config"
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"
import { BigNumber } from "ethers"
import "@nomiclabs/hardhat-waffle"
export default {
solidity: {
compilers: [
{
version: "0.5.16"
},
{
version: "0.6.2"
},
{
version: "0.6.4"
},
{
version: "0.7.0"
},
{
version: "0.8.0"
}
]
},
networks: {
hardhat: {
gasPrice: 225000000000,
chainId: 98200 //Only specify a chainId if we are not forking
},
local: {
url: 'http://localhost:9650/ext/bc/C/rpc',
gasPrice: 225000000000,
chainId: 12345,
accounts: [
"0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027",
"0xbbc2865b76ba28016bc2255c7504d000e046ae01934b04c694592a6276988630",
"0xcdbfd34f687ced8c6968854f8a99ae47712c4f4183b78dcc4a903d1bfe8cbf60",
"0x86f78c5416151fe3546dece84fda4b4b1e36089f2dbc48496faf3a950f16157c",
"0x750839e9dbbd2a0910efe40f50b2f3b2f2f59f5580bb4b83bd8c1201cf9a010a"
]
},
testnet: {
url: 'https://testnet.dijets.io/ext/bc/C/rpc',
gasPrice: 225000000000,
chainId: 98199,
accounts: []
},
mainnet: {
url: 'https://dijets.ukwest.cloudapp.azure.com:443/ext/bc/C/rpc',
gasPrice: 225000000000,
chainId: 98200,
accounts: []
}
}
}
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.
info
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
.
task("accounts", "Prints the list of accounts", async (args, hre): Promise<void> => {
const accounts: SignerWithAddress[] = await hre.ethers.getSigners()
accounts.forEach((account: SignerWithAddress): void => {
console.log(account.address)
})
})
task("balances", "Prints the DJTX account balances for each address", async (args, hre): Promise<void> => {
const accounts: SignerWithAddress[] = await hre.ethers.getSigners()
for(const account of accounts){
const balance: BigNumber = await hre.ethers.provider.getBalance(
account.address
);
console.log(`${account.address} has balance ${balance.toString()}`);
}
})
npx hardhat accounts
prints the list of accounts. npx hardhat balances
prints the list of
DJTX 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.
npx hardhat accounts --network local
Balances#
Prints a list of accounts and their corresponding DJTX balances on the local network started with Dijets-up.
npx hardhat balances --network local
info
The first account with DJTX 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#
task("check-erc20-balance", "Prints out the ERC20 balance of your account").setAction(async function (taskArguments, hre) {
const genericErc20Abi = require("./erc20.abi.json");
const tokenContractAddress = "0x...";
const provider = ethers.getDefaultProvider("https://dijets.ukwest.cloudapp.azure.com:443/ext/bc/C/rpc");
const contract = new ethers.Contract(tokenContractAddress, genericErc20Abi, provider);
const balance = await contract.balanceOf("0x...");
console.log(`Balance in wei: ${balance}`)
});
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
"compile": "npx hardhat compile",
To compile the smart contract run:
npx hardhat compile
Deploy Smart Contracts#
Hardhat enables deploying to multiple environments. You can do so by editing the
deployment script in scripts/deploy.ts
"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:
npx hardhat run scripts/deploy.ts --network mainnet
To deploy the contract to your local network run:
npx hardhat run scripts/deploy.ts --network local
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:
npx hardhat console --network local
Get the contract instance with factory and contract address to interact with our contract:
> const Coin = await ethers.getContractFactory('ExampleERC20'); undefined > const coin = await Coin.attach('0x17aB05351fC94a1a67Bf3f56DdbB941aE6') undefined
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:
> let accounts = await ethers.provider.listAccounts() undefined > accounts [ '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC', '0x78A23300E04FB5d5D2820E23cc679738982e1fd5', '0x3C7daE394BBf8e9EE1359ad14C1C47003bD06293', '0x61e0B3CD93F36847Abbd5d40d6F00a8eC6f3cfFB', '0x0Fa8EA536Be85F32724D57A37758761B86416123' ]
This is exactly the same account list as in yarn accounts
.
Now we can interact with our ERC-20
contract:
> let value = await coin.balanceOf(accounts[0])
undefined
> value.toString()
'123456789'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x00', _isBigNumber: true }
> value.toString()
'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
.
> let result = await coin.transfer(accounts[1], 100)
undefined
> result
{
hash: '0x35eec91011f9089ba7689479617a90baaf8590395b5c80bb209fa7000e4848a5',
type: 0,
accessList: null,
blockHash: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC',
gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },
to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 3,
data: '0xa9059cbb0000000000000000000000009632a79656af553f58738b0fb7503201584959420000000000000000000000000000000000000000000000000000000000000064',
r: '0xc2b9680771c092a106eadb2887e5bff41fcda166c8e00f36ae79b196bbc53d36',
s: '0x355138cb5e2b9f20c15626638750775cfc9423881db374d732a8549d05ebf601',
v: 86260,
creates: null,
chainId: 98200,
wait: [Function (anonymous)]
}
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:
> value = await coin.balanceOf(accounts[0])
BigNumber { _hex: '0x075bccb1', _isBigNumber: true }
> value.toString()
'123456689'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x64', _isBigNumber: true }
> value.toString()
'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.
> let signer1 = await ethers.provider.getSigner(1)
> let contractAsSigner1 = coin.connect(signer1)
Now we can call the contract with signer1
, which is account[1]
.
> await contractAsSigner1.transfer(accounts[0], 5)
{
hash: '0x807947f1c40bb723ac312739d238b62764ae3c3387c6cdbbb6534501577382dd',
type: 0,
accessList: null,
blockHash: null,
blockNumber: null,
transactionIndex: null,
confirmations: 0,
from: '0x9632a79656af553F58738B0FB750320158495942',
gasPrice: BigNumber { _hex: '0x34630b8a00', _isBigNumber: true },
gasLimit: BigNumber { _hex: '0x8754', _isBigNumber: true },
to: '0x17aB05351fC94a1a67Bf3f56DdbB941aE6c63E25',
value: BigNumber { _hex: '0x00', _isBigNumber: true },
nonce: 2,
data: '0xa9059cbb0000000000000000000000008db97c7cece249c2b98bdc0226cc4c2a57bf52fc0000000000000000000000000000000000000000000000000000000000000005',
r: '0xcbf126dd0b109491d037c5f3af754ef2d0d7d06149082b13d0e27e502d3adc5b',
s: '0x5978521804dd15674147cc6b532b8801c4d3a0e94f41f5d7ffaced14b9262504',
v: 86259,
creates: null,
chainId: 98200,
wait: [Function (anonymous)]
}
Let's check balances now:
> value = await coin.balanceOf(accounts[0])
BigNumber { _hex: '0x075bccb6', _isBigNumber: true }
> value.toString()
'123456694'
> value = await coin.balanceOf(accounts[1])
BigNumber { _hex: '0x5f', _isBigNumber: true }
> value.toString()
'95'
Join Dijets Public support space on Qowalts to learn more and ask any questions you may have.