Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎥 Reverted with custom error test args #726

Merged
merged 14 commits into from
May 13, 2022
5 changes: 5 additions & 0 deletions .changeset/thin-items-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ethereum-waffle/chai": patch
---

Test args of custom errors with .withArgs matcher
45 changes: 44 additions & 1 deletion docs/source/migration-guides.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ We updated the following dependencies:

- :code:`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 the :code:`peerDependencies` section - you now need to install :code:`typechain` manually when using Waffle.
- :code:`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 the :code:`peerDependencies` section - you now need to install :code:`ethers` manually when using Waffle.
- :code:`solc` - the package is used by :code:`waffle-compiler` package to provide the default option for compiling Soldity code. Was moved to the :code:`peerDependencies` section and has no version restrictions - you now have to install :code:`solc` manually when using Waffle.
- :code:`solc` - the package is used by :code:`waffle-compiler` package to provide the default option for compiling Solidity code. Was moved to the :code:`peerDependencies` section and has no version restrictions - you now have to install :code:`solc` manually when using Waffle.
- Deprecated :code:`ganache-core` package has been replaced with :code:`ganache` version ^7.0.3. It causes slight differences in the parameters of :code:`MockProvider` from :code:`@ethereum-waffle/provider`. Now the :code:`MockProvider` uses :code:`berlin` hardfork by default.

Changes to :code:`MockProvider` parameters
Expand Down Expand Up @@ -490,3 +490,46 @@ Note that in both cases you can use :code:`chai` negation :code:`not`. In a case
.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:

.. code-block:: solidity

// 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 :code:`.revertedWith` matcher you can capture the custom error's name (:code:`expect(tx).to.be.revertedWith('InsufficientBalance')`). If you want to access arguments of a custom error you should use :code:`.withArgs` matcher after the :code:`.revertedWith` matcher.

.. code-block:: ts

await expect(token.transfer(receiver, 100))
.to.be.revertedWith('InsufficientBalance')
.withArgs(0, 100);

62 changes: 62 additions & 0 deletions waffle-chai/src/call-promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {providers} from 'ethers';

type TransactionResponse = providers.TransactionResponse;
type MaybePromise<T> = T | Promise<T>;

const isTransactionResponse = (response: any): response is TransactionResponse => {
return 'wait' in response;
};

/**
* Takes a chai object (usually a `this` object) and adds a `promise` property to it.
* Adds a `response` property to the chai object with the transaction response.
* The promised is resolved when the transaction is mined.
* Adds a `receipt` property to the chai object with the transaction receipt when the promise is resolved.
* May be called on a chai object which contains any of these:
* - a transaction response
* - a promise which resolves to a transaction response
* - a function that returns a transaction response
* - a function that returns a promise which resolves to a transaction response
* - same combinations as above but query instead of transaction.
* Attention: some matchers require to be called on a transaction.
*/
export const callPromise = (chaiObj: any) => {
if ('callPromise' in chaiObj) {
return;
}

const call = chaiObj._obj;
let response: MaybePromise<any>;

if (typeof call === 'function') {
response = call();
} else {
response = call;
}

if (!('then' in response)) {
if (isTransactionResponse(response)) {
chaiObj.txResponse = response;
chaiObj.callPromise = response.wait().then(txReceipt => {
chaiObj.txReceipt = txReceipt;
});
} else {
chaiObj.queryResponse = response;
chaiObj.callPromise = Promise.resolve();
}
} else {
chaiObj.callPromise = response.then(async (response: any) => {
if (isTransactionResponse(response)) {
chaiObj.txResponse = response;
const txReceipt = await response.wait();
chaiObj.txReceipt = txReceipt;
} else {
chaiObj.queryResponse = response;
}
});
}

// Setting `then` and `catch` on the chai object to be compliant with the chai-aspromised library.
chaiObj.then = chaiObj.callPromise.then.bind(chaiObj.callPromise);
chaiObj.catch = chaiObj.callPromise.catch.bind(chaiObj.callPromise);
};
2 changes: 2 additions & 0 deletions waffle-chai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {supportChangeTokenBalance} from './matchers/changeTokenBalance';
import {supportChangeTokenBalances} from './matchers/changeTokenBalances';
import {supportCalledOnContract} from './matchers/calledOnContract/calledOnContract';
import {supportCalledOnContractWith} from './matchers/calledOnContract/calledOnContractWith';
import {supportWithArgs} from './matchers/withArgs';

export function waffleChai(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils) {
supportBigNumber(chai.Assertion, utils);
Expand All @@ -33,4 +34,5 @@ export function waffleChai(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils) {
supportChangeTokenBalances(chai.Assertion);
supportCalledOnContract(chai.Assertion);
supportCalledOnContractWith(chai.Assertion);
supportWithArgs(chai.Assertion);
}
11 changes: 7 additions & 4 deletions waffle-chai/src/matchers/changeBalance.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import {BigNumber, BigNumberish} from 'ethers';
import {Account, getAddressOf} from './misc/account';
import {getBalanceChange} from './changeEtherBalance';
import {transactionPromise} from '../transaction-promise';
import {callPromise} from '../call-promise';

export function supportChangeBalance(Assertion: Chai.AssertionStatic) {
Assertion.addMethod('changeBalance', function (
this: any,
account: Account,
balanceChange: BigNumberish
) {
transactionPromise(this);
callPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
const derivedPromise = this.callPromise.then(() => {
if (!('txResponse' in this)) {
throw new Error('The changeBalance matcher must be called on a transaction');
}
return Promise.all([
getBalanceChange(this.txResponse, account, {includeFee: true}),
getAddressOf(account)
Expand All @@ -31,7 +34,7 @@ export function supportChangeBalance(Assertion: Chai.AssertionStatic) {
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.txPromise = derivedPromise;
this.callPromise = derivedPromise;
return this;
});
}
11 changes: 7 additions & 4 deletions waffle-chai/src/matchers/changeBalances.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BigNumber, BigNumberish} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {callPromise} from '../call-promise';
import {getBalanceChanges} from './changeEtherBalances';
import {Account} from './misc/account';
import {getAddresses} from './misc/balance';
Expand All @@ -10,9 +10,12 @@ export function supportChangeBalances(Assertion: Chai.AssertionStatic) {
accounts: Account[],
balanceChanges: BigNumberish[]
) {
transactionPromise(this);
callPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
const derivedPromise = this.callPromise.then(() => {
if (!('txResponse' in this)) {
throw new Error('The changeBalances matcher must be called on a transaction');
}
return Promise.all([
getBalanceChanges(this.txResponse, accounts, {includeFee: true}),
getAddresses(accounts)
Expand All @@ -34,7 +37,7 @@ export function supportChangeBalances(Assertion: Chai.AssertionStatic) {
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.txPromise = derivedPromise;
this.callPromise = derivedPromise;
return this;
});
}
11 changes: 7 additions & 4 deletions waffle-chai/src/matchers/changeEtherBalance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BigNumber, BigNumberish, providers} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {callPromise} from '../call-promise';
import {ensure} from './calledOnContract/utils';
import {Account, getAddressOf} from './misc/account';
import {BalanceChangeOptions} from './misc/balance';
Expand All @@ -11,9 +11,12 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) {
balanceChange: BigNumberish,
options: BalanceChangeOptions
) {
transactionPromise(this);
callPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
const derivedPromise = this.callPromise.then(() => {
if (!('txResponse' in this)) {
throw new Error('The changeEtherBalance matcher must be called on a transaction');
}
return Promise.all([
getBalanceChange(this.txResponse, account, options),
getAddressOf(account)
Expand All @@ -34,7 +37,7 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) {
);
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.txPromise = derivedPromise;
this.callPromise = derivedPromise;
return this;
});
}
Expand Down
11 changes: 7 additions & 4 deletions waffle-chai/src/matchers/changeEtherBalances.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BigNumber, BigNumberish, providers} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {callPromise} from '../call-promise';
import {getAddressOf, Account} from './misc/account';
import {BalanceChangeOptions, getAddresses, getBalances} from './misc/balance';

Expand All @@ -10,9 +10,12 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) {
balanceChanges: BigNumberish[],
options: BalanceChangeOptions
) {
transactionPromise(this);
callPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(() => {
const derivedPromise = this.callPromise.then(() => {
if (!('txResponse' in this)) {
throw new Error('The changeEtherBalances matcher must be called on a transaction');
}
return Promise.all([
getBalanceChanges(this.txResponse, accounts, options),
getAddresses(accounts)
Expand All @@ -34,7 +37,7 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) {
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.txPromise = derivedPromise;
this.callPromise = derivedPromise;
return this;
});
}
Expand Down
11 changes: 7 additions & 4 deletions waffle-chai/src/matchers/changeTokenBalance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BigNumber, BigNumberish, Contract, providers} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {callPromise} from '../call-promise';
import {Account, getAddressOf} from './misc/account';

export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
Expand All @@ -9,9 +9,12 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
account: Account,
balanceChange: BigNumberish
) {
transactionPromise(this);
callPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(async () => {
const derivedPromise = this.callPromise.then(async () => {
if (!('txReceipt' in this)) {
throw new Error('The changeTokenBalance matcher must be called on a transaction');
}
const address = await getAddressOf(account);
const actualChanges = await getBalanceChange(this.txReceipt, token, address);
return [actualChanges, address];
Expand All @@ -30,7 +33,7 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.txPromise = derivedPromise;
this.callPromise = derivedPromise;
return this;
});
}
Expand Down
11 changes: 7 additions & 4 deletions waffle-chai/src/matchers/changeTokenBalances.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {BigNumber, BigNumberish, Contract, providers} from 'ethers';
import {transactionPromise} from '../transaction-promise';
import {callPromise} from '../call-promise';
import {Account, getAddressOf} from './misc/account';

export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
Expand All @@ -9,9 +9,12 @@ export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
accounts: Account[],
balanceChanges: BigNumberish[]
) {
transactionPromise(this);
callPromise(this);
const isNegated = this.__flags.negate === true;
const derivedPromise = this.txPromise.then(async () => {
const derivedPromise = this.callPromise.then(async () => {
if (!('txReceipt' in this)) {
throw new Error('The changeTokenBalances matcher must be called on a transaction');
}
const addresses = await getAddresses(accounts);
const actualChanges = await getBalanceChanges(this.txReceipt, token, addresses);
return [actualChanges, addresses];
Expand All @@ -32,7 +35,7 @@ export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
});
this.then = derivedPromise.then.bind(derivedPromise);
this.catch = derivedPromise.catch.bind(derivedPromise);
this.txPromise = derivedPromise;
this.callPromise = derivedPromise;
return this;
});
}
Expand Down
Loading