Home

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.

Need some help?

You can join Dijets Support Space for a live chat with one of the team members. Join us on Qowalts.