Introduction to Ethers.js
This article will use several examples to outline the common usage of Ethers.js.
All examples in this article use Ethers.js version 6.12.0
Official reference documentation: https://docs.ethers.org/v6
Overview
Ethers.js mainly consists of the following components:
Provider
A Provider is a read-only connection to the blockchain, allowing queries of blockchain state, such as account, block, or transaction details, querying event logs, or evaluating read-only code.
Signer
A Signer wraps all operations that interact with an account. Each account has a private key that can be used to sign various operations.
The private key may be in memory (using Wallet) or protected through some IPC layer, such as MetaMask, which proxies interactions from websites to the browser plugin, keeping the private key away from the site and only allowing interaction after requesting and receiving user authorization.
Transaction
To make any state changes to the blockchain, a transaction is required, which incurs a fee covering the costs associated with executing the transaction (e.g., reading disk and performing calculations) and storing updated information.
If a transaction reverts, a fee is still required as validators still must spend resources to attempt to run the transaction to determine it has reverted and record details of its failure.
Transactions include sending ether from one user to another, deploying contracts, or executing state-changing operations against contracts.
Contract
A Contract is a program deployed to the blockchain that contains some code and has allocated storage that can be read from and written to.
When connected to a Provider, read-only operations can be called, or state-changing operations can be called when connected to a Signer.
Receipt
Once a transaction is submitted to the blockchain, it is placed in the mempool until a validator decides to include it.
Changes only occur after a transaction is included in the blockchain, at which point a receipt is received containing details about the transaction, such as which block it was included in, the actual fee paid, the Gas used, and all events it emitted, as well as whether it was successful or reverted.
Balance Query
In this section, we'll implement a script to query Ethereum balance. To achieve this functionality, we need to connect to an Ethereum node. Of course, we don't need to deploy an Ethereum node just to query the balance; there are many tools available in the market. Here we use INFURA
to access an Ethereum node: https://app.infura.io/
After registration, you can obtain a free API Key. Find your Endpoints, and you can directly access Ethereum.
After obtaining the node, you can use Provider
to query the Ethereum balance of any address.
const { ethers } = require("ethers"); const INFURA_API_KEY = "..."; const ADDRESS = "..."; const provider = new ethers.JsonRpcProvider( `https://mainnet.infura.io/v3/${INFURA_API_KEY}` ); const getBalance = async () => { const balance = await provider.getBalance(ADDRESS); console.log( `ETH Balance of ${ADDRESS} --> ${ethers.formatEther(balance)} ETH\n` ); }; getBalance();
Reading Contracts
In this section, we'll take DAI
(a stablecoin) as an example. First, find the DAI contract on etherscan, where you can see all the read-only contract functions of DAI and interact with them directly.
Address:https://etherscan.io/token/0x6b175474e89094c44da98b954eedeac495271d0f#readContract
What we need to do is use Ethers.js to achieve the same effect.
DAI is an ERC20 contract, meaning it implements a series of functions, such as the common totalSupply, balanceOf, name, etc.
The Ethers.js library can receive an ABI array to understand how to interact with the contract.
After creating the contract
instance, you can call the functions defined in ERC20_ABI
through it.
const { ethers } = require("ethers"); const INFURA_API_KEY = "..."; const DAI_TOKEN_ADDRESS = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; const provider = new ethers.JsonRpcProvider( `https://mainnet.infura.io/v3/${INFURA_API_KEY}` ); const ERC20_ABI = [ "function name() view returns (string)", "function symbol() view returns (string)", "function totalSupply() view returns (uint256)", "function balanceOf(address) view returns (uint256)", ]; const contract = new ethers.Contract(DAI_TOKEN_ADDRESS, ERC20_ABI, provider) const main = async () => { const name = await contract.name(); const symbol = await contract.symbol(); const totalSupply = await contract.totalSupply(); console.log(`Token Name: ${name}`); console.log(`Token Symbol: ${symbol}`); console.log(`Total Supply: ${totalSupply}`); const balance = await contract.balanceOf("0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11") console.log(`Balance: ${ethers.formatUnits(balance)}`); }; main();
Running results:
Token Name: Dai Stablecoin
Token Symbol: DAI
Total Supply: 3213933035046622754824652548
Balance: 7288394.450956033361944629
Sending Transactions
In this section, we'll implement sending tokens from one wallet to another using Ethers.js.
We'll use the Sepolia test network here.
The main idea is to define the sender's and receiver's addresses, then get the sender's private key, create a wallet object with this private key, and then call the sendTransaction method to send tokens.
The implementation logic is simple, and the specific code is as follows:
const { ethers } = require("ethers"); const INFURA_API_KEY = "..."; const provider = new ethers.JsonRpcProvider( `https://sepolia.infura.io/v3/${INFURA_API_KEY}` ); const sender = "0x3Ee7EbFb5823c8C8af05BC371873D371085447D1"; const receiver = "0x8cC4f3fBa4b89da6e700D5602e7622CDc0444B62"; const senderPrivateKey = "..."; const wallet = new ethers.Wallet(senderPrivateKey, provider); const main = async () => { const senderBalanceBefore = await provider.getBalance(sender); const receiverBalanceBefore = await provider.getBalance(receiver); console.log( `Sender balance before --> ${ethers.formatEther(senderBalanceBefore)} ETH` ); console.log( `Receiver balance before --> ${ethers.formatEther(receiverBalanceBefore)} ETH` ); // Send 0.001 ETH to receiver const tx = await wallet.sendTransaction({ to: receiver, value: ethers.parseEther("0.001"), }); // Wait for the transaction to be mined await tx.wait(); console.log(tx); const senderBalanceAfter = await provider.getBalance(sender); const receiverBalanceAfter = await provider.getBalance(receiver); console.log( `Sender balance After --> ${ethers.formatEther(senderBalanceAfter)} ETH` ); console.log( `Receiver balance After --> ${ethers.formatEther(receiverBalanceAfter)} ETH` ); }; main();
Running results:
Sender balance before --> 0.029886093476039388 ETH
Receiver balance before --> 0.1 ETH
TransactionResponse {
provider: JsonRpcProvider {},
blockNumber: null,
blockHash: null,
index: undefined,
hash: '0x15b13a89590c9e56fe93b91d610255487f2b2a22a17bf917a87f9d30aad043ad',
type: 2,
to: '0x8cC4f3fBa4b89da6e700D5602e7622CDc0444B62',
from: '0x3Ee7EbFb5823c8C8af05BC371873D371085447D1',
nonce: 40,
gasLimit: 21000n,
gasPrice: undefined,
maxPriorityFeePerGas: 546176991n,
maxFeePerGas: 564848631n,
maxFeePerBlobGas: null,
data: '0x',
value: 1000000000000000n,
chainId: 11155111n,
signature: Signature { r: "0xf2667f4712bab6df16ff902127c5410c74165886de2941843bca53a28440e5b4", s: "0x09bafe8c0e7f7ab7fa573205c606f490a14654c44c86f8047bcae1d88ee2661a", yParity: 0, networkV: null },
accessList: [],
blobVersionedHashes: null
}
Sender balance After --> 0.028874431147816388 ETH
Receiver balance After --> 0.101 ETH
Contract Transfer
In this section, we'll implement a transfer using the contract's own transfer function.
We're still using the Sepolia test network, but this time we're using the Link token, which is also an ERC20 token. You can get some test tokens through this link: https://faucets.chain.link/sepolia
Find the contract address for this token on etherscan: https://sepolia.etherscan.io/token/0xdbc1856cbd9553b8f2be31f6e6d5695dc823b47c
Since it's an ERC20 token, the ABI for common functions like transfer is fixed, so you can define it in an array and use it just like in the previous section.
The specific transfer code is as follows:
const { ethers } = require("ethers"); const INFURA_API_KEY = "..."; const provider = new ethers.JsonRpcProvider( `https://sepolia.infura.io/v3/${INFURA_API_KEY}` ); const sender = "0x3Ee7EbFb5823c8C8af05BC371873D371085447D1"; const receiver = "0x8cC4f3fBa4b89da6e700D5602e7622CDc0444B62"; const senderPrivateKey = "..."; const wallet = new ethers.Wallet(senderPrivateKey, provider); const ERC20_ABI = [ "function balanceOf(address) view returns (uint256)", "function transfer(address to, uint256 amount) returns (bool)", ]; const ChainLinkTokenAddress = "0x779877A7B0D9E8603169DdbD7836e478b4624789"; const contract = new ethers.Contract( ChainLinkTokenAddress, ERC20_ABI, provider ); const main = async () => { const senderBalanceBefore = await contract.balanceOf(sender); const receiverBalanceBefore = await contract.balanceOf(receiver); console.log( `Sender balance --> ${ethers.formatEther(senderBalanceBefore)} Link` ); console.log( `Receiver balance --> ${ethers.formatEther(receiverBalanceBefore)} Link` ); const contractWithWallect = contract.connect(wallet); const txResponse = await contractWithWallect.transfer( receiver, ethers.parseEther("1") ); const receipt = await txResponse.wait(); console.log(receipt); const senderBalanceAfter = await contract.balanceOf(sender); const receiverBalanceAfter = await contract.balanceOf(receiver); console.log( `Sender balance --> ${ethers.formatEther(senderBalanceAfter)} Link` ); console.log( `Receiver balance --> ${ethers.formatEther(receiverBalanceAfter)} Link` ); }; main()
Running results:
Sender balance --> 40.0 Link
Receiver balance --> 0.0 Link
ContractTransactionReceipt {
provider: JsonRpcProvider {},
to: '0x779877A7B0D9E8603169DdbD7836e478b4624789',
from: '0x3Ee7EbFb5823c8C8af05BC371873D371085447D1',
contractAddress: null,
hash: '0x8d6ecd27959f86d87b06ae30184f2c97d78064af5d20b49dd4c338990d176b31',
index: 63,
blockHash: '0xce05ec859ecc91dd060756ba1680a28f2940e57a7a12aca631ff8741fe2bea46',
blockNumber: 5775997,
logsBloom: '0x00000004000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000010000000000000000000000000000400000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000002004000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000002000000000',
gasUsed: 51646n,
blobGasUsed: null,
cumulativeGasUsed: 12901369n,
gasPrice: 663138827n,
blobGasPrice: null,
type: 2,
status: 1,
root: undefined
}
Sender balance --> 39.0 Link
Receiver balance --> 1.0 Link
Event Query
Ethers.js can also query specific events. For example, under the ERC20 standard, a Transfer event is triggered every time a token is transferred. These events are recorded in the blockchain, and we can query the corresponding events through Ethers.js.
const { ethers } = require("ethers"); const INFURA_API_KEY = "..."; const DAI_TOKEN_ADDRESS = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; const provider = new ethers.JsonRpcProvider( `https://mainnet.infura.io/v3/${INFURA_API_KEY}` ); const ERC20_ABI = [ "function name() view returns (string)", "function symbol() view returns (string)", "function totalSupply() view returns (uint256)", "function balanceOf(address) view returns (uint256)", "event Transfer(address indexed from, address indexed to, uint256 value)", ]; const contract = new ethers.Contract(DAI_TOKEN_ADDRESS, ERC20_ABI, provider); const main = async () => { const block = await provider.getBlockNumber(); const transferEvents = await contract.queryFilter( "Transfer", block - 1, block ); console.log(transferEvents); }; main();
Author: LeapWhale
Copyright: This article is copyrighted by LeapWhale. If reproduced, please cite the source!