Creating Dapps with ethers.js

We will now create a front-end web application for the Counter contract we deployed in the previous sections using ethers, a JavaScript library for interacting with the EVM.

Setting up an Ethers project

It is generally better practice (for security reasons) to copy the ethers library to your own webserver and serve it yourself. More over, third party libraries should be monitored and updated regularly to avoid attacks involving supply chain vulnerabilities.

Fixing the library’s version and using tools like Github’s Dependabot and its security updates can help developers keep their dependencies updated and their dApps safe.

Our web application will have only an HTML file

For this tutorial, load ethers from their CDN to include it into the web application.

<script type="module">
  import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";
  // Your code here...
</script>

Connecting to the EVM

Adding the Contract details to the project

To create a contract instance, ethers requires the contract address and its ABI. You can get both from either Remix or the Hardhat project.

If you are using Remix, the ABI will be avaiblable to copy and paste after compiling the contract. And the address can be copied from the Deployed Contracts section in the Deploy & Run tab.

If you are using Hardhat, you can use the address logged by the deploy script and the ABI code that should be stored in the artifacts folder as Counter.json.

Include both the address and the ABI in the script.js file.

Important: when using ethers.js to interact with Metamask, different version of ethers.js requires different ways to initialize providers.

For ethers.js v6 and later

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

For ethers.js v5.7 and older

const provider = new ethers.providers.Web3Provider(window.ethereum, "any");
await provider.send("eth_requestAccounts", []);
const signer = provider.getSigner();

The below code is written using ethers.js v5.7

import { ethers } from 'ethers';

const CounterContractAddress = "CONTRACT_ADDRESS"; // Replace with actual contract address
const CounterContractABI = []; // Replace with actual contract ABI

let CounterContract;

const provider = new ethers.providers.Web3Provider(window.ethereum);
  provider.send("eth_requestAccounts", []).then(() => {
    provider.listAccounts().then((accounts) => {
      if (accounts.length === 0) throw new Error('No account found.');
      
      const signer = provider.getSigner(accounts[0]); // Use the first account

      /* 3.1 Create instance of the Counter contract */
      CounterContract = new ethers.Contract(
        CounterContractAddress,
        CounterContractABI,
        signer
      );
  });
});

RPC connections

The quickest way to connect to an EVM is using the MetaMask browser extension.

You can prompt the user to add CC3 Testnet by using the wallet_addEthereumChain method.

window.ethereum.request({
    method: "wallet_addEthereumChain",
    params: [{
        chainId: "0x89",
        rpcUrls: ["https://rpc.cc3-testnet.creditcoin.network"],
        chainName: "Creditcoin Testnet",
        nativeCurrency: {
            name: "CTC",
            symbol: "CTC",
            decimals: 18
        },
        blockExplorerUrls: ["https://creditcoin-testnet.blockscout.com/"]
    }]
});

Building a simple UI

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="script.js" type="module" defer></script> 
</head>
<body>
    <div id="counter"></div>
    <div id="count">Empty</div>
    <button id="increment">Increment</button>
    <button id="decrement">Decrement</button>
</body>
</html>

script.js

import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js";

let provider;
let signer = null;
let CounterContract;

const CounterContractAddress = "DEPLOYED_COUNTER_CONTRACT_ADDRESS";
const CounterContractABI = [
	{
		"inputs": [],
		"name": "decrementCounter",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "getCount",
		"outputs": [
			{
				"internalType": "int256",
				"name": "",
				"type": "int256"
			}
		],
		"stateMutability": "view",
		"type": "function"
	},
	{
		"inputs": [],
		"name": "incrementCounter",
		"outputs": [],
		"stateMutability": "nonpayable",
		"type": "function"
	}
];

window.ethereum.request({
  method: "wallet_addEthereumChain",
  params: [{
      chainId: "0x18e90",
      rpcUrls: ["https://rpc.cc3-testnet.creditcoin.network"],
      chainName: "Creditcoin Testnet",
      nativeCurrency: {
          name: "CTC",
          symbol: "CTC",
          decimals: 18
      },
      blockExplorerUrls: ["https://creditcoin-testnet.blockscout.com/"]
  }]
});

if (window.ethereum == null) {

    // If MetaMask is not installed, we use the default provider,
    // which is backed by a variety of third-party services (such
    // as INFURA). They do not have private keys installed,
    // so they only have read-only access
    console.log("MetaMask not installed; using read-only defaults")
    provider = ethers.getDefaultProvider()

} else {

    // Connect to the MetaMask EIP-1193 object. This is a standard
    // protocol that allows Ethers access to make all read-only
    // requests through MetaMask.
    provider = new ethers.BrowserProvider(window.ethereum)

    // It also provides an opportunity to request access to write
    // operations, which will be performed by the private key
    // that MetaMask manages for the user.
    signer = await provider.getSigner();
}

// Initialize the CounterContract instance
CounterContract = new ethers.Contract(
  CounterContractAddress,
  CounterContractABI,
  signer
);

// Refresh count function
export const refreshCount = async () => {
  const count = await CounterContract.getCount();
  document.getElementById("count").innerHTML = count;
}

// Get the actual count from the chain and update element
refreshCount();

// Create contract increment function for button
export const increment = async () => {
  const tx = await CounterContract.incrementCounter();
  await tx.wait();
  refreshCount();
};

// Create contract decrement function for button
export const decrement = async () => {
  const tx = await CounterContract.decrementCounter();
  await tx.wait();
  refreshCount();
};

// Add increment behavior to Increment button
document.getElementById("increment").addEventListener("click", increment);

// Add decrement behavior to Decrement button
document.getElementById("decrement").addEventListener("click", decrement);

Last updated