Migration: From Ethers v4
This document only covers the features present in v4 which have changed in some important way in v5.
It does not cover all the new additional features that have been added and mainly aims to help those updating their older scripts and applications to retain functional parity.
If you encounter any missing changes, please let me know and I'll update this guide.
Since BigNumber is used quite frequently, it has been moved to the top level of the umbrella package.
ethers.utils.BigNumber
ethers.utils.BigNumberish
ethers.BigNumber
ethers.BigNumberish
The bigNumberify
method was always preferred over the constructor since it could short-circuit an object instantiation for [[BigNumber] objects (since they are immutable). This has been moved to a static from
class method.
new ethers.utils.BigNumber(someValue)
ethers.utils.bigNumberify(someValue);
ethers.BigNumber.from(someValue)
The name of the resolved address has changed. If the address passed into the constructor was an ENS name, the address will be resolved before any calls are made to the contract.
The name of the property where the resolved address has changed from addressPromise
to resolvedAddress
.
contract.addressPromise
contract.resolvedAddress
The only difference in gas estimation is that the bucket has changed its name from estimate
to estimateGas
.
contract.estimate.transfer(toAddress, amount)
contract.estimateGas.transfer(toAddress, amount)
In a contract in ethers, there is a functions
bucket, which exposes all the methods of a contract.
All these functions are available on the root contract itself as well and historically there was no difference between contact.foo
and contract.functions.foo
. The original reason for the functions
bucket was to help when there were method names that collided with other buckets, which is rare.
In v5, the functions
bucket is now intended to help with frameworks and for the new error recovery API, so most users should use the methods on the root contract.
The main difference will occur when a contract method only returns a single item. The root method will dereference this automatically while the functions
bucket will preserve it as an Result.
If a method returns multiple items, there is no difference.
This helps when creating a framework, since the result will always be known to have the same number of components as the Fragment outputs, without having to handle the special case of a single return value.
const abi = [
"function single() view returns (uint8)",
"function double() view returns (uint8, uint8)",
];
await contract.single()
await contract.functions.single()
await contract.single()
await contract.functions.single()
await contract.double()
await contract.functions.double()
await contract.double()
await contract.functions.double()
All errors now belong to the Logger class and the related functions have been moved to Logger instances, which can include a per-package version string.
Global error functions have been moved to Logger class methods.
ethers.errors.UNKNOWN_ERROR
ethers.errors.*
errors.setCensorship(censorship, permanent)
errors.setLogLevel(logLevel)
errors.checkArgumentCount(count, expectedCount, suffix)
errors.checkNew(self, kind)
errors.checkNormalize()
errors.throwError(message, code, params)
errors.warn(...)
errors.info(...)
ethers.utils.Logger.errors.UNKNOWN_ERROR
ethers.utils.Logger.errors.*
Logger.setCensorship(censorship, permanent)
Logger.setLogLevel(logLevel)
const logger = new ethers.utils.Logger(version);
logger.checkArgumentCount(count, expectedCount, suffix)
logger.checkNew(self, kind)
logger.checkNormalize()
logger.throwError(message, code, params)
logger.warn(...)
logger.info(...)
The Interface object has undergone the most dramatic changes.
It is no longer a meta-class and now has methods that simplify handling contract interface operations without the need for object inspection and special edge cases.
interface.functions.transfer.encode(to, amount)
interface.functions.transfer.decode(callData)
interface.encodeFunctionData("transfer", [ to, amount ])
interface.decodeFunctionResult("transfer", data)
interface.encodeFunctionData("transfer(address,uint)", [ to, amount ])
interface.decodeFunctionResult("transfer(address to, uint256 amount)", data)
interface.events.Transfer.encodeTopics(values)
interface.events.Transfer.decode(data, topics)
interface.encodeFilterTopics("Transfer", values)
interface.decodeEventLog("Transfer", data, topics)
Interrogating properties about a function or event can now (mostly) be done directly on the Fragment object.
// v4
interface.functions.transfer.name
interface.functions.transfer.inputs
interface.functions.transfer.outputs
interface.functions.transfer.payable
interface.functions.transfer.gas
// v5
const functionFragment = interface.getFunction("transfer")
functionFragment.name
functionFragment.inputs
functionFragment.outputs
functionFragment.payable
functionFragment.gas
// v4; type is "call" or "transaction"
interface.functions.transfer.type
// v5; constant is true (i.e. "call") or false (i.e. "transaction")
functionFragment.constant
// v4
interface.events.Transfer.anonymous
interface.events.Transfer.inputs
interface.events.Transfer.name
// v5
const eventFragment = interface.getEvent("Transfer");
eventFragment.anonymous
eventFragment.inputs
eventFragment.name
// v4
const functionSig = interface.functions.transfer.signature
const sighash = interface.functions.transfer.sighash
const eventSig = interface.events.Transfer.signature
const topic = interface.events.Transfer.topic
// v5
const functionSig = functionFragment.format()
const sighash = interface.getSighash(functionFragment)
const eventSig = eventFragment.format()
const topic = interface.getTopic(eventFragment)
The mnemonic phrase and related properties have been merged into a single mnemonic
object, which also now includes the locale
.
wallet.mnemonic
wallet.path
wallet.mnemonic.phrase
wallet.mnemonic.path