Skip to content

Commit

Permalink
🎥 Reverted with custom error test args (#726)
Browse files Browse the repository at this point in the history
  • Loading branch information
yivlad committed May 13, 2022
1 parent 1e598c5 commit 79a421f
Show file tree
Hide file tree
Showing 18 changed files with 408 additions and 222 deletions.
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

0 comments on commit 79a421f

Please sign in to comment.