Migration guides¶
Migration from Waffle 2.2.0 to Waffle 2.3.0¶
Created monorepo¶
Waffle was internally migrated to a monorepo. Thanks to this, you can now use parts of waffle individually. We provide the following packages:
ethereum-waffle
- core package exporting everythingethereum-compiler
- compile your contracts programmaticallyethereum-chai
- chai matchers for better unit testingethereum-provider
- mock provider to interact with an in-memory blockchain
Created MockProvider class¶
We added MockProvider class. It changed the creation of the provider.
Waffle 2.2.0
await createMockProvider(options);
Waffle 2.3.0
provider = new MockProvider();
Reorganise getWallets() method¶
Waffle 2.2.0
await getWallets(provider);
Waffle 2.3.0
new MockProvider().getWallets()
Migration from Waffle 2.3.0 to Waffle 2.4.0¶
Renamed configuration options¶
We renamed configuration options to compile contracts:
sourcesPath
- renamed to sourceDirectorytargetPath
- renamed to outputDirectorynpmPath
- renamed to nodeModulesDirectorycompiler
- renamed to compilerTypedocker-tag
- replaced by compilerVersionsolcVersion
- replaced by compilerVersionlegacyOutput
- removed, setting it to false gave no effectallowedPaths
- renamed to compilerAllowedPathsganacheOptions
- removed, wasn’t used by the compiler
Migration from Waffle 2.5.* to Waffle 3.0.0¶
There are some new functionality and some slight refactoring and improved paradigms in Waffle v3.
Removed deprecated APIs from the provider¶
In Waffle 3.0.0
we remove deprecated APIs from the provider, such as createMockProvider
and getGanacheOptions
.
Swapped arguments for Fixture¶
In Waffle 3.0.0
we swapped arguments for Fixture, because the provider argument is very rarely used compared to wallets.
So such implementation should be more convenient for users.
Waffle 2.5.0
function createFixtureLoader(overrideProvider?: MockProvider, overrideWallets?: Wallet[]);
Waffle 3.0.0
function createFixtureLoader(overrideWallets?: Wallet[], overrideProvider?: MockProvider);
Waffle 2.5.0
import {expect} from 'chai';
import {loadFixture, deployContract} from 'ethereum-waffle';
import BasicTokenMock from './build/BasicTokenMock';
describe('Fixtures', () => {
async function fixture(provider, [wallet, other]) {
const token = await deployContract(wallet, BasicTokenMock, [
wallet.address, 1000
]);
return {token, wallet, other};
}
it('Assigns initial balance', async () => {
const {token, wallet} = await loadFixture(fixture);
expect(await token.balanceOf(wallet.address)).to.equal(1000);
});
});
Waffle 3.0.0
import {expect} from 'chai';
import {loadFixture, deployContract} from 'ethereum-waffle';
import BasicTokenMock from './build/BasicTokenMock';
describe('Fixtures', () => {
async function fixture([wallet, other], provider) {
const token = await deployContract(wallet, BasicTokenMock, [
wallet.address, 1000
]);
return {token, wallet, other};
}
it('Assigns initial balance', async () => {
const {token, wallet} = await loadFixture(fixture);
expect(await token.balanceOf(wallet.address)).to.equal(1000);
});
});
Added automatic recognising waffle.json config without cli argument¶
Waffle recognises waffle.json
as the default configuration file. If your configuration file is called
waffle.json
, it’s possible to use just waffle
to build contracts.
In Waffle 2.5.0, If the argument has not been provided, the Waffle uses the default configuration.
Waffle 2.5.0
{
"scripts": {
"build": "waffle waffle.json"
}
}
Waffle 3.0.0
{
"scripts": {
"build": "waffle"
}
}
Introduced MockProviderOptions¶
We added MockProviderOptions. It will be convenient in the future, when the provider may need some
options other than ganacheOptions
.
Waffle 2.5.0
import {expect} from 'chai';
import {Wallet} from 'ethers';
import {MockProvider} from 'ethereum-waffle';
import {deployToken} from './BasicToken';
describe('INTEGRATION: MockProvider', () => {
it('accepts options', () => {
const original = Wallet.createRandom();
const provider = new MockProvider({
accounts: [{balance: '100', secretKey: original.privateKey}]
});
const wallets = provider.getWallets();
expect(wallets.length).to.equal(1);
expect(wallets[0].address).to.equal(original.address);
});
});
Waffle 3.0.0
import {expect} from 'chai';
import {Wallet} from 'ethers';
import {MockProvider} from 'ethereum-waffle';
import {deployToken} from './BasicToken';
describe('INTEGRATION: MockProvider', () => {
it('accepts options', () => {
const original = Wallet.createRandom();
const provider = new MockProvider({
ganacheOptions: {
accounts: [{balance: '100', secretKey: original.privateKey}]
}
});
const wallets = provider.getWallets();
expect(wallets.length).to.equal(1);
expect(wallets[0].address).to.equal(original.address);
});
});
Dropped support for contract interface¶
We dropped support for contract interface because it duplicated contract ABI. Also interface
is a keyword in typescript,
so we decided not to use this field. Now we support just contract.abi
.
Waffle 2.5.0
{
"abi": [
...
],
"interface: [
...
],
"evm": {
...
},
"bytecode": "..."
}
Waffle 3.0.0
{
"abi": [
{...}
],
"evm": {
...
},
"bytecode": "..."
}
Migration from Waffle 3.4.0 to Waffle 4.0.0-alpha¶
Dependencies upgrades¶
The main difference between Waffle 3.4.0 and Waffle 4.0.0-alpha is about dependencies that Waffle packages use. We updated the following dependencies:
typechain
- bumped version from ^2.0.0 to ^9.0.0. Now every Waffle package uses the same version of the package. Also the package was moved to thepeerDependencies
section - you now need to installtypechain
manually when using Waffle.ethers
- bumped version from to ^5.5.4. Now every Waffle package uses the same version of the package. Also the package was moved to thepeerDependencies
section - you now need to installethers
manually when using Waffle.solc
- the package is used bywaffle-compiler
package to provide the default option for compiling Solidity code. Was moved to thepeerDependencies
section and has no version restrictions - you now have to installsolc
manually when using Waffle.Deprecated
ganache-core
package has been replaced withganache
version ^7.0.3. It causes slight differences in the parameters ofMockProvider
from@ethereum-waffle/provider
. Now theMockProvider
usesberlin
hardfork by default.
Changes to MockProvider
parameters¶
Previous (optional) parameters of MockProvider
included override options for the Ganache provider:
interface MockProviderOptions {
ganacheOptions: {
account_keys_path?: string;
accounts?: object[];
allowUnlimitedContractSize?: boolean;
blockTime?: number;
db_path?: string;
debug?: boolean;
default_balance_ether?: number;
fork?: string | object;
fork_block_number?: string | number;
forkCacheSize?: number;
gasLimit?: string | number;
gasPrice?: string;
hardfork?: "byzantium" | "constantinople" | "petersburg" | "istanbul" | "muirGlacier";
hd_path?: string;
locked?: boolean;
logger?: {
log(msg: string): void;
};
mnemonic?: string;
network_id?: number;
networkId?: number;
port?: number;
seed?: any;
time?: Date;
total_accounts?: number;
unlocked_accounts?: string[];
verbose?: boolean;
vmErrorsOnRPCResponse?: boolean;
ws?: boolean;
}
}
Current ganacheOptions
parameter are documented here.
Typechain changes¶
If you used type generation (typechainEnabled
option set to true
in waffle.json
), you need to update your code to conform to the new naming convention used by typechain
. Contract factories now have postfix __factory
instead of Factory
. For example, MyContractFactory
becomes MyContract__factory
. Example refactoring:
const contractConstructorArgs: [string, string] = [bob.address, charlie.address];
-const contract = await deployContract(alice, MyContractFactory, contractConstructorArgs);
+const contract = await deployContract(alice, MyContract__factory, contractConstructorArgs);
@ethereum-waffle/jest
deprecated¶
We stopped supporting @ethereum-waffle/jest
. From now on the package is deprecated and will be removed in the future. The suggested test framework to use with Waffle
is mocha
combined with chai
and @ethereum-waffle/chai
package. If you want to migrate from jest to mocha in your project, please follow the guide below.
Setup
mocha
- this may include the following steps:
installing
mocha
andchai
packages.if you are using
typescript
, installing@types/mocha
,@types/chai
andts-node
packages.updating your
test
script (the common pattern for typescript is"test": "mocha -r ts-node/register/transpile-only 'test/**/*.test.ts'"
).updating your tests to use
mocha
syntax.
Replace
@ethereum-waffle/jest
with@ethereum-waffle/chai
. Below is little table that contains the list of all the matchers provided by@ethereum-waffle/jest
and their replacements in@ethereum-waffle/chai
.
@ethereum-waffle/jest |
@ethereum-waffle/chai |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Time-based tests¶
If your tests rely on manually setting timestamp on the blockchain using evm_mine, you need to use evm_setTime alongside.
For example:
export async function mineBlock(provider: providers.Web3Provider, timestamp: number) {
+provider.send('evm_setTime', [timestamp * 1000])
await provider.send('evm_mine', [timestamp])
}
Tests relying on setting gasPrice¶
Since London hardfork, baseFeePerGas
is replacing gasPrice
.
If your tests are relying on setting gasPrice
with changeBalance
matcher, you will have to update them.
Before
await expect(() =>
sender.sendTransaction({
to: receiver.address,
gasPrice: 0,
value: 200
})
).to.changeBalance(sender, -200);
After
const TX_GAS = 21000;
const BASE_FEE_PER_GAS = 875000000
const gasFees = BASE_FEE_PER_GAS * TX_GAS;
await expect(() =>
sender.sendTransaction({
to: receiver.address,
gasPrice: BASE_FEE_PER_GAS,
value: 200
})
).to.changeBalance(sender, -(gasFees + 200));
Currently there is no way to set gasPrice
to 0
in Ganache
.
Instead of (deprecated) matcher changeBalance
, new matcher changeEtherBalance
can be used instead - which handles transaction fee calculation automatically.
Custom wallet mnemonic¶
Ganache has a build-in set of Wallets with positive Ether balance. In the new Ganache, you should not override the wallet config, otherwise you might end up with Wallets with no Ether balance.
const provider = new MockProvider({
ganacheOptions: {
// Remove this, if exists:
wallet: {
mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn'
}
}
})
Chaining matchers¶
Now when testing events on a smart contract you can conveniently chain matchers. It can be especially useful when testing events.
const tx = await contract.emitEventsOneAndTwo();
await expect(tx)
.to.emit(contract, 'One').withArgs(
1,
'One'
)
.to.emit(contract, 'Two').withArgs(
2,
'Two'
)
.to.not.emit(contract, 'Three');
changeEtherBalance
, changeEtherBalances
, changeTokenbalance
and changeTokenBalances
matchers also support chaining:
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(tx)
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
.and.to.emit(complex, 'TransferredEther').withArgs(200)
.and.to.emit(complex, 'TransferredTokens').withArgs(100);
Although you may find it more convenient to write multiple expects. The test below is equivalent to the one above:
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(tx).to.changeTokenBalances(token, [sender, receiver], [-100, 100]);
await expect(tx).to.changeEtherBalances([sender, receiver], [-200, 200]);
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
Note that in both cases you can use chai
negation not
. In a case of a single expect everything after not
is negated.
await token.approve(complex.address, 100);
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
await expect(expect(tx)
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
.and.not
.to.emit(complex, 'UnusedEvent') // This is negated
.and.to.changeEtherBalances([sender, receiver], [-100, 100]) // This is negated as well
Custom errors¶
Custom errors were introduced in Solidity v0.8.4. It is a convenient and gas-efficient way to explain to users why an operation failed. Custom errors are defined in a similar way as events:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Insufficient balance for transfer. Needed `required` but only
/// `available` available.
/// @param available balance available.
/// @param required requested amount to transfer.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
mapping(address => uint) balance;
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
// Error call using named parameters. Equivalent to
// revert InsufficientBalance(balance[msg.sender], amount);
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
When using Waffle v4.0.0-alpha.* with Hardhat, you can test transactions being reverted with custom errors as well. Using the .revertedWith
matcher you can capture the custom error’s name (expect(tx).to.be.revertedWith('InsufficientBalance')
). If you want to access arguments of a custom error you should use .withArgs
matcher after the .revertedWith
matcher.
await expect(token.transfer(receiver, 100))
.to.be.revertedWith('InsufficientBalance')
.withArgs(0, 100);