ethers.js v6
ethers.js is a TypeScript/JavaScript library for interacting with EVM-compatible blockchains. Version 6 is a ground-up rewrite with native ES2020 bigint, tree-shakeable exports, and a cleaner API surface. It provides Providers (read-only chain access), Signers (transaction authorization), and Contract objects (typed on-chain interaction).
What You Probably Got Wrong
LLMs consistently produce v5 code when asked for ethers.js. Every pattern below is a v6 correction. If you see
BigNumber,ethers.utils, orethers.providersin your output, you are writing v5. Stop and use the v6 equivalents.
- v6 uses native
bigint, NOTBigNumber. There is noBigNumberclass in v6. All uint256 values are returned asbigint. Do not importBigNumberfrom anywhere. Do not call.toNumber(),.toString(), or.mul()on return values -- use native bigint operators (+,-,*,/,%,**). ethers.parseEther(), NOTethers.utils.parseEther(). Theutilsnamespace does not exist in v6. All utility functions are top-level exports:parseEther,formatEther,parseUnits,formatUnits,id,keccak256,toUtf8Bytes,AbiCoder, etc.ethers.providersdoes not exist. Provider classes are top-level:JsonRpcProvider,BrowserProvider,WebSocketProvider,FallbackProvider,InfuraProvider,AlchemyProvider. Do not writenew ethers.providers.JsonRpcProvider().BrowserProviderreplacedWeb3Provider. To wrap a browser wallet (MetaMask'swindow.ethereum), usenew BrowserProvider(window.ethereum), notnew Web3Provider(window.ethereum).- Contract constructor changed. v5:
new ethers.Contract(address, abi, signerOrProvider). v6: same signature, but the third argument is now called a "runner" and can be a Provider (read-only) or Signer (read-write). The.connect()method returns a new Contract instance with a different runner. contract.getFunction("name")for explicit method access. v6 still supportscontract.functionName()but the recommended pattern for clarity iscontract.getFunction("transfer").WalletextendsAbstractSigner, NOTSigner. The class hierarchy changed.Wallettakes a private key and a Provider.new Wallet(privateKey, provider).getDefaultProvider()is still available but rate-limited. For production, always use an explicitJsonRpcProviderwith your own RPC endpoint.- Event filters use
contract.on("EventName", callback). The v6 event API is similar to v5 but the filter builder changed. Usecontract.filters.EventName(arg1, arg2)-- returns an ethersDeferredTopicFilter. receipt.statusis a number (0 or 1), not a string. Checkreceipt.status === 1for success.TransactionResponse.wait()can returnnull. If the transaction is dropped or replaced,wait()returnsnull. Always handle this case.- ENS resolution only works on networks that support it. Calling
provider.resolveName("vitalik.eth")on a non-mainnet provider without ENS deployment returnsnull, not an error. Interfacereplacedethers.utils.Interface. Usenew Interface(abi)directly, imported fromethers.
Installation
npm install ethers
v6 requires Node.js 16+ and TypeScript 5.0+ (if using TypeScript).
{
"dependencies": {
"ethers": "^6.13.0"
}
}
Quick Start
Create a Provider (Read-Only)
import { JsonRpcProvider } from "ethers";
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error("RPC_URL environment variable required");
const provider = new JsonRpcProvider(RPC_URL);
const blockNumber = await provider.getBlockNumber();
console.log(`Current block: ${blockNumber}`);
Create a Wallet (Read-Write)
import { JsonRpcProvider, Wallet } from "ethers";
const RPC_URL = process.env.RPC_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
if (!RPC_URL) throw new Error("RPC_URL required");
if (!PRIVATE_KEY) throw new Error("PRIVATE_KEY required");
const provider = new JsonRpcProvider(RPC_URL);
const wallet = new Wallet(PRIVATE_KEY, provider);
console.log(`Address: ${wallet.address}`);
Browser Wallet (MetaMask, etc.)
import { BrowserProvider } from "ethers";
// window.ethereum is the EIP-1193 provider injected by the wallet
if (!window.ethereum) throw new Error("No wallet detected");
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
console.log(`Connected: ${await signer.getAddress()}`);
Providers
JsonRpcProvider
Direct connection to any JSON-RPC endpoint. Use for server-side scripts, bots, and backends.
import { JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
const balance = await provider.getBalance("vitalik.eth");
console.log(`Balance: ${balance} wei`);
const network = await provider.getNetwork();
console.log(`Chain ID: ${network.chainId}`);
WebSocketProvider
Persistent connection for real-time event subscriptions. Required for provider.on("block", ...).
import { WebSocketProvider } from "ethers";
const wsProvider = new WebSocketProvider("wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
wsProvider.on("block", (blockNumber: number) => {
console.log(`New block: ${blockNumber}`);
});
BrowserProvider
Wraps any EIP-1193 provider (MetaMask, WalletConnect, Coinbase Wallet). Use in frontend dApps.
import { BrowserProvider } from "ethers";
const provider = new BrowserProvider(window.ethereum);
// Request account access (triggers wallet popup)
const signer = await provider.getSigner();
const address = await signer.getAddress();
const balance = await provider.getBalance(address);
FallbackProvider
Redundant provider with quorum voting. Uses multiple backends and returns the result agreed upon by a quorum.
import { FallbackProvider, JsonRpcProvider } from "ethers";
const providers = [
new JsonRpcProvider("https://rpc1.example.com"),
new JsonRpcProvider("https://rpc2.example.com"),
new JsonRpcProvider("https://rpc3.example.com"),
];
// quorum: 2 means at least 2 providers must agree
const fallbackProvider = new FallbackProvider(providers, undefined, { quorum: 2 });
Signers and Wallets
Wallet from Private Key
import { Wallet, JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
console.log(`Address: ${wallet.address}`);
console.log(`Chain: ${(await wallet.provider!.getNetwork()).chainId}`);
Wallet from Mnemonic
import { Wallet, Mnemonic } from "ethers";
// BIP-39 mnemonic to HD wallet (default path: m/44'/60'/0'/0/0)
const mnemonic = Mnemonic.fromPhrase("test test test test test test test test test test test junk");
const wallet = Wallet.fromPhrase(mnemonic.phrase);
console.log(`Address: ${wallet.address}`);
console.log(`Private key: ${wallet.privateKey}`);
Random Wallet
import { Wallet } from "ethers";
const randomWallet = Wallet.createRandom();
console.log(`Address: ${randomWallet.address}`);
console.log(`Mnemonic: ${randomWallet.mnemonic?.phrase}`);
Signing Messages
import { Wallet, verifyMessage } from "ethers";
const wallet = new Wallet(process.env.PRIVATE_KEY!);
const message = "Hello, Ethereum!";
const signature = await wallet.signMessage(message);
// Recover signer address from signature
const recovered = verifyMessage(message, signature);
console.log(`Signer: ${recovered}`);
console.log(`Match: ${recovered === wallet.address}`);
Signing Typed Data (EIP-712)
import { Wallet } from "ethers";
const wallet = new Wallet(process.env.PRIVATE_KEY!);
const domain = {
name: "MyDApp",
version: "1",
chainId: 1n,
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
};
const types = {
Transfer: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "nonce", type: "uint256" },
],
};
const value = {
to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
amount: 1000000n,
nonce: 0n,
};
const signature = await wallet.signTypedData(domain, types, value);
Contracts
Read-Only Contract (Provider)
import { Contract, JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function balanceOf(address) view returns (uint256)",
"function totalSupply() view returns (uint256)",
];
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const contract = new Contract(USDC, ERC20_ABI, provider);
const [name, symbol, decimals, totalSupply] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
contract.totalSupply(),
]);
// decimals returns bigint in v6
console.log(`${name} (${symbol}), decimals: ${decimals}`);
console.log(`Total supply: ${totalSupply}`); // bigint
Read-Write Contract (Signer)
import { Contract, Wallet, JsonRpcProvider, parseUnits } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const ERC20_ABI = [
"function transfer(address to, uint256 amount) returns (bool)",
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
];
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const contract = new Contract(USDC, ERC20_ABI, wallet);
const recipient = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
// 100 USDC (6 decimals)
const amount = parseUnits("100", 6);
const tx = await contract.transfer(recipient, amount);
console.log(`TX hash: ${tx.hash}`);
const receipt = await tx.wait();
if (receipt === null) throw new Error("Transaction dropped or replaced");
if (receipt.status !== 1) throw new Error("Transaction reverted");
console.log(`Confirmed in block ${receipt.blockNumber}`);
Human-Readable ABI
v6 supports human-readable ABI strings. Use these for simple interactions instead of full JSON ABIs.
const abi = [
"function name() view returns (string)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
"error InsufficientBalance(uint256 available, uint256 required)",
];
Full JSON ABI
For complex contracts or when you need type generation, use the full JSON ABI.
const abi = [
{
name: "transfer",
type: "function",
stateMutability: "nonpayable",
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
] as const;
ContractFactory (Deploying Contracts)
import { ContractFactory, Wallet, JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const abi = [
"constructor(string name, string symbol, uint8 decimals)",
"function name() view returns (string)",
"function symbol() view returns (string)",
];
// bytecode from solc compilation output
const bytecode = "0x608060...";
const factory = new ContractFactory(abi, bytecode, wallet);
const contract = await factory.deploy("MyToken", "MTK", 18);
const deployTx = contract.deploymentTransaction();
if (!deployTx) throw new Error("No deployment transaction");
console.log(`Deploying: ${deployTx.hash}`);
await contract.waitForDeployment();
const address = await contract.getAddress();
console.log(`Deployed at: ${address}`);
Connecting a Contract to a Different Runner
import { Contract, JsonRpcProvider, Wallet } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const abi = ["function transfer(address to, uint256 amount) returns (bool)"];
const address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
// Read-only contract
const readContract = new Contract(address, abi, provider);
// Connect to signer for write operations -- returns a NEW instance
const writeContract = readContract.connect(wallet) as Contract;
Utility Functions
Parsing and Formatting
import { parseEther, formatEther, parseUnits, formatUnits } from "ethers";
// ETH (18 decimals)
const weiAmount: bigint = parseEther("1.5"); // 1500000000000000000n
const ethAmount: string = formatEther(weiAmount); // "1.5"
// Custom decimals (USDC = 6)
const usdcWei: bigint = parseUnits("100.50", 6); // 100500000n
const usdcStr: string = formatUnits(usdcWei, 6); // "100.5"
// Gwei (9 decimals)
const gweiWei: bigint = parseUnits("30", "gwei"); // 30000000000n
const gweiStr: string = formatUnits(gweiWei, "gwei"); // "30.0"
Hashing
import { keccak256, toUtf8Bytes, id, solidityPackedKeccak256 } from "ethers";
// keccak256 of raw bytes
const hash = keccak256(toUtf8Bytes("Hello"));
// id() is shorthand for keccak256(toUtf8Bytes(...))
const eventTopic = id("Transfer(address,address,uint256)");
// Solidity-style packed encoding then keccak256
const packed = solidityPackedKeccak256(
["address", "uint256"],
["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 42n]
);
ABI Encoding and Decoding
import { AbiCoder } from "ethers";
const coder = AbiCoder.defaultAbiCoder();
// Encode
const encoded = coder.encode(
["address", "uint256", "bool"],
["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 1000n, true]
);
// Decode
const [addr, amount, flag] = coder.decode(
["address", "uint256", "bool"],
encoded
);
console.log(`Address: ${addr}, Amount: ${amount}, Flag: ${flag}`);
Interface for Encoding Function Calls
import { Interface } from "ethers";
const iface = new Interface([
"function transfer(address to, uint256 amount) returns (bool)",
"function approve(address spender, uint256 amount) returns (bool)",
]);
// Encode a function call
const calldata = iface.encodeFunctionData("transfer", [
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
parseUnits("100", 6),
]);
// Decode a function call
const decoded = iface.decodeFunctionData("transfer", calldata);
console.log(`To: ${decoded[0]}, Amount: ${decoded[1]}`);
// Decode function result
const resultData = "0x0000000000000000000000000000000000000000000000000000000000000001";
const result = iface.decodeFunctionResult("transfer", resultData);
console.log(`Success: ${result[0]}`);
Transactions
Send ETH
import { Wallet, JsonRpcProvider, parseEther } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const tx = await wallet.sendTransaction({
to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
value: parseEther("0.1"),
});
console.log(`TX hash: ${tx.hash}`);
const receipt = await tx.wait();
if (receipt === null) throw new Error("Transaction dropped or replaced");
if (receipt.status !== 1) throw new Error("Transaction reverted");
console.log(`Gas used: ${receipt.gasUsed}`);
console.log(`Effective gas price: ${receipt.gasPrice}`);
EIP-1559 Transaction with Gas Control
import { Wallet, JsonRpcProvider, parseEther, parseUnits } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const feeData = await provider.getFeeData();
const tx = await wallet.sendTransaction({
to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
value: parseEther("0.1"),
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
});
const receipt = await tx.wait();
if (receipt === null) throw new Error("Transaction dropped or replaced");
if (receipt.status !== 1) throw new Error("Transaction reverted");
Transaction Receipt Details
import { JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const receipt = await provider.getTransactionReceipt("0x...");
if (!receipt) throw new Error("Transaction not found");
console.log(`Status: ${receipt.status === 1 ? "success" : "reverted"}`);
console.log(`Block: ${receipt.blockNumber}`);
console.log(`Gas used: ${receipt.gasUsed}`);
console.log(`Logs: ${receipt.logs.length}`);
Events
Listening to Events (Real-Time)
import { Contract, WebSocketProvider } from "ethers";
const provider = new WebSocketProvider("wss://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const abi = [
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const contract = new Contract(USDC, abi, provider);
contract.on("Transfer", (from: string, to: string, value: bigint, event) => {
console.log(`Transfer: ${from} -> ${to}: ${value}`);
console.log(`Block: ${event.log.blockNumber}`);
});
Querying Past Events
import { Contract, JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const abi = [
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const contract = new Contract(USDC, abi, provider);
// Query last 1000 blocks
const currentBlock = await provider.getBlockNumber();
const events = await contract.queryFilter("Transfer", currentBlock - 1000, currentBlock);
for (const event of events) {
if (event.args) {
console.log(`${event.args.from} -> ${event.args.to}: ${event.args.value}`);
}
}
Filtered Events
import { Contract, JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const abi = [
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const contract = new Contract(USDC, abi, provider);
// Filter: transfers FROM a specific address
const fromFilter = contract.filters.Transfer("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
// Filter: transfers TO a specific address
const toFilter = contract.filters.Transfer(null, "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
const currentBlock = await provider.getBlockNumber();
const transfers = await contract.queryFilter(fromFilter, currentBlock - 5000, currentBlock);
console.log(`Found ${transfers.length} transfers`);
ENS Resolution
import { JsonRpcProvider } from "ethers";
// ENS resolution requires a mainnet provider (or a network with ENS deployed)
const provider = new JsonRpcProvider(process.env.RPC_URL);
// Forward resolution: name -> address
const address = await provider.resolveName("vitalik.eth");
if (address === null) throw new Error("ENS name not found");
console.log(`vitalik.eth = ${address}`);
// Reverse resolution: address -> name
const name = await provider.lookupAddress("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
console.log(`Name: ${name}`); // may be null if no reverse record
ENS Avatar
import { JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const resolver = await provider.getResolver("vitalik.eth");
if (!resolver) throw new Error("No resolver for this name");
const avatar = await resolver.getAvatar();
console.log(`Avatar: ${avatar}`);
const contentHash = await resolver.getContentHash();
console.log(`Content hash: ${contentHash}`);
Error Handling
Catching Contract Reverts
import { Contract, JsonRpcProvider, isError } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const abi = [
"function transfer(address to, uint256 amount) returns (bool)",
"error InsufficientBalance(uint256 available, uint256 required)",
];
const contract = new Contract("0x...", abi, provider);
try {
const tx = await contract.transfer("0x...", 1000n);
const receipt = await tx.wait();
if (receipt === null) throw new Error("Transaction dropped");
if (receipt.status !== 1) throw new Error("Transaction reverted");
} catch (error: unknown) {
if (isError(error, "CALL_EXCEPTION")) {
// Contract revert with reason or custom error
console.error(`Revert reason: ${error.reason}`);
console.error(`Error data: ${error.data}`);
} else if (isError(error, "INSUFFICIENT_FUNDS")) {
console.error("Not enough ETH for gas + value");
} else if (isError(error, "NONCE_EXPIRED")) {
console.error("Nonce already used -- transaction may be a duplicate");
} else if (isError(error, "REPLACEMENT_UNDERPRICED")) {
console.error("Replacement transaction gas price too low");
} else if (isError(error, "NETWORK_ERROR")) {
console.error("Network connectivity issue");
} else if (isError(error, "TIMEOUT")) {
console.error("Request timed out");
} else {
throw error;
}
}
The isError Utility
v6 provides isError(error, code) for type-safe error checking. This replaces the v5 pattern of checking error.code directly.
import { isError } from "ethers";
try {
await provider.getBlock("invalid");
} catch (error: unknown) {
if (isError(error, "INVALID_ARGUMENT")) {
console.error(`Invalid argument: ${error.argument} = ${error.value}`);
} else if (isError(error, "SERVER_ERROR")) {
console.error(`RPC error: ${error.info}`);
} else {
throw error;
}
}
Multicall Pattern
Batch multiple read calls into a single RPC request using Multicall3.
import { Contract, JsonRpcProvider, Interface } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
// Multicall3 is deployed at the same address on all major EVM chains
const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
const multicallAbi = [
"function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) view returns (tuple(bool success, bytes returnData)[])",
];
const multicall = new Contract(MULTICALL3, multicallAbi, provider);
const erc20Iface = new Interface([
"function balanceOf(address) view returns (uint256)",
"function decimals() view returns (uint8)",
]);
const tokens = [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
"0xdAC17F958D2ee523a2206206994597C13D831ec7", // USDT
"0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI
];
const wallet = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";
const calls = tokens.map((token) => ({
target: token,
allowFailure: false,
callData: erc20Iface.encodeFunctionData("balanceOf", [wallet]),
}));
const results = await multicall.aggregate3(calls);
for (let i = 0; i < tokens.length; i++) {
const balance = erc20Iface.decodeFunctionResult("balanceOf", results[i].returnData)[0];
console.log(`${tokens[i]}: ${balance}`);
}
v5 to v6 Migration Quick Reference
| v5 Pattern | v6 Equivalent |
|---|---|
ethers.providers.JsonRpcProvider |
JsonRpcProvider (top-level import) |
ethers.providers.Web3Provider |
BrowserProvider |
ethers.utils.parseEther("1.0") |
parseEther("1.0") |
ethers.utils.formatEther(wei) |
formatEther(wei) |
ethers.utils.parseUnits("100", 6) |
parseUnits("100", 6) |
ethers.utils.formatUnits(val, 6) |
formatUnits(val, 6) |
ethers.utils.keccak256(...) |
keccak256(...) |
ethers.utils.id(...) |
id(...) |
ethers.utils.Interface |
Interface |
ethers.utils.AbiCoder |
AbiCoder |
ethers.BigNumber.from(...) |
Native BigInt(...) or 123n |
bn.add(other) |
a + b |
bn.mul(other) |
a * b |
bn.div(other) |
a / b |
bn.eq(other) |
a === b |
bn.gt(other) |
a > b |
bn.isZero() |
a === 0n |
bn.toNumber() |
Number(a) (only if value fits in Number.MAX_SAFE_INTEGER) |
bn.toString() |
a.toString() |
contract.deployed() |
contract.waitForDeployment() |
contract.address |
await contract.getAddress() |
tx.wait() returns receipt |
tx.wait() returns receipt or null |
receipt.status === 1 |
Same (unchanged) |
Common Patterns
Check Balance Before Transfer
import { JsonRpcProvider, Wallet, parseEther, formatEther } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
async function safeTransferEth(to: string, ethAmount: string): Promise<string> {
const value = parseEther(ethAmount);
const balance = await provider.getBalance(wallet.address);
const feeData = await provider.getFeeData();
// 21000 gas for simple ETH transfer
const estimatedGasCost = 21000n * (feeData.maxFeePerGas ?? feeData.gasPrice ?? 0n);
const totalCost = value + estimatedGasCost;
if (balance < totalCost) {
throw new Error(
`Insufficient balance. Have: ${formatEther(balance)} ETH, Need: ${formatEther(totalCost)} ETH`
);
}
const tx = await wallet.sendTransaction({ to, value });
const receipt = await tx.wait();
if (receipt === null) throw new Error("Transaction dropped or replaced");
if (receipt.status !== 1) throw new Error("Transaction reverted");
return tx.hash;
}
ERC-20 Approve and Transfer
import { Contract, Wallet, JsonRpcProvider, parseUnits, MaxUint256 } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const ERC20_ABI = [
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function transferFrom(address from, address to, uint256 amount) returns (bool)",
];
async function ensureAllowance(
tokenAddress: string,
spender: string,
amount: bigint
): Promise<void> {
const token = new Contract(tokenAddress, ERC20_ABI, wallet);
const currentAllowance: bigint = await token.allowance(wallet.address, spender);
if (currentAllowance >= amount) return;
const tx = await token.approve(spender, amount);
const receipt = await tx.wait();
if (receipt === null) throw new Error("Approval transaction dropped");
if (receipt.status !== 1) throw new Error("Approval reverted");
}
Wait for N Confirmations
import { Wallet, JsonRpcProvider, parseEther } from "ethers";
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const tx = await wallet.sendTransaction({
to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
value: parseEther("0.01"),
});
// Wait for 3 confirmations
const receipt = await tx.wait(3);
if (receipt === null) throw new Error("Transaction dropped or replaced");
if (receipt.status !== 1) throw new Error("Transaction reverted");
console.log(`Confirmed with ${3} confirmations at block ${receipt.blockNumber}`);
Reference Links
- Official docs: https://docs.ethers.org/v6/
- Migration guide: https://docs.ethers.org/v6/migrating/
- GitHub: https://github.com/ethers-io/ethers.js
- NPM: https://www.npmjs.com/package/ethers
- Changelog: https://github.com/ethers-io/ethers.js/blob/main/CHANGELOG.md