Skip to content

Commit 79a421f

Browse files
authored
🎥 Reverted with custom error test args (#726)
1 parent 1e598c5 commit 79a421f

18 files changed

+408
-222
lines changed

‎.changeset/thin-items-hide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ethereum-waffle/chai": patch
3+
---
4+
5+
Test args of custom errors with .withArgs matcher

‎docs/source/migration-guides.rst

+44-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ We updated the following dependencies:
263263

264264
- :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.
265265
- :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.
266-
- :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.
266+
- :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.
267267
- 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.
268268

269269
Changes to :code:`MockProvider` parameters
@@ -490,3 +490,46 @@ Note that in both cases you can use :code:`chai` negation :code:`not`. In a case
490490
.and.not
491491
.to.emit(complex, 'UnusedEvent') // This is negated
492492
.and.to.changeEtherBalances([sender, receiver], [-100, 100]) // This is negated as well
493+
494+
495+
Custom errors
496+
~~~~~~~~~~~~~
497+
498+
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:
499+
500+
.. code-block:: solidity
501+
502+
// SPDX-License-Identifier: GPL-3.0
503+
pragma solidity ^0.8.4;
504+
505+
/// Insufficient balance for transfer. Needed `required` but only
506+
/// `available` available.
507+
/// @param available balance available.
508+
/// @param required requested amount to transfer.
509+
error InsufficientBalance(uint256 available, uint256 required);
510+
511+
contract TestToken {
512+
mapping(address => uint) balance;
513+
function transfer(address to, uint256 amount) public {
514+
if (amount > balance[msg.sender])
515+
// Error call using named parameters. Equivalent to
516+
// revert InsufficientBalance(balance[msg.sender], amount);
517+
revert InsufficientBalance({
518+
available: balance[msg.sender],
519+
required: amount
520+
});
521+
balance[msg.sender] -= amount;
522+
balance[to] += amount;
523+
}
524+
// ...
525+
}
526+
527+
528+
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.
529+
530+
.. code-block:: ts
531+
532+
await expect(token.transfer(receiver, 100))
533+
.to.be.revertedWith('InsufficientBalance')
534+
.withArgs(0, 100);
535+

‎waffle-chai/src/call-promise.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {providers} from 'ethers';
2+
3+
type TransactionResponse = providers.TransactionResponse;
4+
type MaybePromise<T> = T | Promise<T>;
5+
6+
const isTransactionResponse = (response: any): response is TransactionResponse => {
7+
return 'wait' in response;
8+
};
9+
10+
/**
11+
* Takes a chai object (usually a `this` object) and adds a `promise` property to it.
12+
* Adds a `response` property to the chai object with the transaction response.
13+
* The promised is resolved when the transaction is mined.
14+
* Adds a `receipt` property to the chai object with the transaction receipt when the promise is resolved.
15+
* May be called on a chai object which contains any of these:
16+
* - a transaction response
17+
* - a promise which resolves to a transaction response
18+
* - a function that returns a transaction response
19+
* - a function that returns a promise which resolves to a transaction response
20+
* - same combinations as above but query instead of transaction.
21+
* Attention: some matchers require to be called on a transaction.
22+
*/
23+
export const callPromise = (chaiObj: any) => {
24+
if ('callPromise' in chaiObj) {
25+
return;
26+
}
27+
28+
const call = chaiObj._obj;
29+
let response: MaybePromise<any>;
30+
31+
if (typeof call === 'function') {
32+
response = call();
33+
} else {
34+
response = call;
35+
}
36+
37+
if (!('then' in response)) {
38+
if (isTransactionResponse(response)) {
39+
chaiObj.txResponse = response;
40+
chaiObj.callPromise = response.wait().then(txReceipt => {
41+
chaiObj.txReceipt = txReceipt;
42+
});
43+
} else {
44+
chaiObj.queryResponse = response;
45+
chaiObj.callPromise = Promise.resolve();
46+
}
47+
} else {
48+
chaiObj.callPromise = response.then(async (response: any) => {
49+
if (isTransactionResponse(response)) {
50+
chaiObj.txResponse = response;
51+
const txReceipt = await response.wait();
52+
chaiObj.txReceipt = txReceipt;
53+
} else {
54+
chaiObj.queryResponse = response;
55+
}
56+
});
57+
}
58+
59+
// Setting `then` and `catch` on the chai object to be compliant with the chai-aspromised library.
60+
chaiObj.then = chaiObj.callPromise.then.bind(chaiObj.callPromise);
61+
chaiObj.catch = chaiObj.callPromise.catch.bind(chaiObj.callPromise);
62+
};

‎waffle-chai/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {supportChangeTokenBalance} from './matchers/changeTokenBalance';
1515
import {supportChangeTokenBalances} from './matchers/changeTokenBalances';
1616
import {supportCalledOnContract} from './matchers/calledOnContract/calledOnContract';
1717
import {supportCalledOnContractWith} from './matchers/calledOnContract/calledOnContractWith';
18+
import {supportWithArgs} from './matchers/withArgs';
1819

1920
export function waffleChai(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils) {
2021
supportBigNumber(chai.Assertion, utils);
@@ -33,4 +34,5 @@ export function waffleChai(chai: Chai.ChaiStatic, utils: Chai.ChaiUtils) {
3334
supportChangeTokenBalances(chai.Assertion);
3435
supportCalledOnContract(chai.Assertion);
3536
supportCalledOnContractWith(chai.Assertion);
37+
supportWithArgs(chai.Assertion);
3638
}

‎waffle-chai/src/matchers/changeBalance.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import {BigNumber, BigNumberish} from 'ethers';
22
import {Account, getAddressOf} from './misc/account';
33
import {getBalanceChange} from './changeEtherBalance';
4-
import {transactionPromise} from '../transaction-promise';
4+
import {callPromise} from '../call-promise';
55

66
export function supportChangeBalance(Assertion: Chai.AssertionStatic) {
77
Assertion.addMethod('changeBalance', function (
88
this: any,
99
account: Account,
1010
balanceChange: BigNumberish
1111
) {
12-
transactionPromise(this);
12+
callPromise(this);
1313
const isNegated = this.__flags.negate === true;
14-
const derivedPromise = this.txPromise.then(() => {
14+
const derivedPromise = this.callPromise.then(() => {
15+
if (!('txResponse' in this)) {
16+
throw new Error('The changeBalance matcher must be called on a transaction');
17+
}
1518
return Promise.all([
1619
getBalanceChange(this.txResponse, account, {includeFee: true}),
1720
getAddressOf(account)
@@ -31,7 +34,7 @@ export function supportChangeBalance(Assertion: Chai.AssertionStatic) {
3134
});
3235
this.then = derivedPromise.then.bind(derivedPromise);
3336
this.catch = derivedPromise.catch.bind(derivedPromise);
34-
this.txPromise = derivedPromise;
37+
this.callPromise = derivedPromise;
3538
return this;
3639
});
3740
}

‎waffle-chai/src/matchers/changeBalances.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {BigNumber, BigNumberish} from 'ethers';
2-
import {transactionPromise} from '../transaction-promise';
2+
import {callPromise} from '../call-promise';
33
import {getBalanceChanges} from './changeEtherBalances';
44
import {Account} from './misc/account';
55
import {getAddresses} from './misc/balance';
@@ -10,9 +10,12 @@ export function supportChangeBalances(Assertion: Chai.AssertionStatic) {
1010
accounts: Account[],
1111
balanceChanges: BigNumberish[]
1212
) {
13-
transactionPromise(this);
13+
callPromise(this);
1414
const isNegated = this.__flags.negate === true;
15-
const derivedPromise = this.txPromise.then(() => {
15+
const derivedPromise = this.callPromise.then(() => {
16+
if (!('txResponse' in this)) {
17+
throw new Error('The changeBalances matcher must be called on a transaction');
18+
}
1619
return Promise.all([
1720
getBalanceChanges(this.txResponse, accounts, {includeFee: true}),
1821
getAddresses(accounts)
@@ -34,7 +37,7 @@ export function supportChangeBalances(Assertion: Chai.AssertionStatic) {
3437
});
3538
this.then = derivedPromise.then.bind(derivedPromise);
3639
this.catch = derivedPromise.catch.bind(derivedPromise);
37-
this.txPromise = derivedPromise;
40+
this.callPromise = derivedPromise;
3841
return this;
3942
});
4043
}

‎waffle-chai/src/matchers/changeEtherBalance.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {BigNumber, BigNumberish, providers} from 'ethers';
2-
import {transactionPromise} from '../transaction-promise';
2+
import {callPromise} from '../call-promise';
33
import {ensure} from './calledOnContract/utils';
44
import {Account, getAddressOf} from './misc/account';
55
import {BalanceChangeOptions} from './misc/balance';
@@ -11,9 +11,12 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) {
1111
balanceChange: BigNumberish,
1212
options: BalanceChangeOptions
1313
) {
14-
transactionPromise(this);
14+
callPromise(this);
1515
const isNegated = this.__flags.negate === true;
16-
const derivedPromise = this.txPromise.then(() => {
16+
const derivedPromise = this.callPromise.then(() => {
17+
if (!('txResponse' in this)) {
18+
throw new Error('The changeEtherBalance matcher must be called on a transaction');
19+
}
1720
return Promise.all([
1821
getBalanceChange(this.txResponse, account, options),
1922
getAddressOf(account)
@@ -34,7 +37,7 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) {
3437
);
3538
this.then = derivedPromise.then.bind(derivedPromise);
3639
this.catch = derivedPromise.catch.bind(derivedPromise);
37-
this.txPromise = derivedPromise;
40+
this.callPromise = derivedPromise;
3841
return this;
3942
});
4043
}

‎waffle-chai/src/matchers/changeEtherBalances.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {BigNumber, BigNumberish, providers} from 'ethers';
2-
import {transactionPromise} from '../transaction-promise';
2+
import {callPromise} from '../call-promise';
33
import {getAddressOf, Account} from './misc/account';
44
import {BalanceChangeOptions, getAddresses, getBalances} from './misc/balance';
55

@@ -10,9 +10,12 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) {
1010
balanceChanges: BigNumberish[],
1111
options: BalanceChangeOptions
1212
) {
13-
transactionPromise(this);
13+
callPromise(this);
1414
const isNegated = this.__flags.negate === true;
15-
const derivedPromise = this.txPromise.then(() => {
15+
const derivedPromise = this.callPromise.then(() => {
16+
if (!('txResponse' in this)) {
17+
throw new Error('The changeEtherBalances matcher must be called on a transaction');
18+
}
1619
return Promise.all([
1720
getBalanceChanges(this.txResponse, accounts, options),
1821
getAddresses(accounts)
@@ -34,7 +37,7 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) {
3437
});
3538
this.then = derivedPromise.then.bind(derivedPromise);
3639
this.catch = derivedPromise.catch.bind(derivedPromise);
37-
this.txPromise = derivedPromise;
40+
this.callPromise = derivedPromise;
3841
return this;
3942
});
4043
}

‎waffle-chai/src/matchers/changeTokenBalance.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {BigNumber, BigNumberish, Contract, providers} from 'ethers';
2-
import {transactionPromise} from '../transaction-promise';
2+
import {callPromise} from '../call-promise';
33
import {Account, getAddressOf} from './misc/account';
44

55
export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
@@ -9,9 +9,12 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
99
account: Account,
1010
balanceChange: BigNumberish
1111
) {
12-
transactionPromise(this);
12+
callPromise(this);
1313
const isNegated = this.__flags.negate === true;
14-
const derivedPromise = this.txPromise.then(async () => {
14+
const derivedPromise = this.callPromise.then(async () => {
15+
if (!('txReceipt' in this)) {
16+
throw new Error('The changeTokenBalance matcher must be called on a transaction');
17+
}
1518
const address = await getAddressOf(account);
1619
const actualChanges = await getBalanceChange(this.txReceipt, token, address);
1720
return [actualChanges, address];
@@ -30,7 +33,7 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
3033
});
3134
this.then = derivedPromise.then.bind(derivedPromise);
3235
this.catch = derivedPromise.catch.bind(derivedPromise);
33-
this.txPromise = derivedPromise;
36+
this.callPromise = derivedPromise;
3437
return this;
3538
});
3639
}

‎waffle-chai/src/matchers/changeTokenBalances.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {BigNumber, BigNumberish, Contract, providers} from 'ethers';
2-
import {transactionPromise} from '../transaction-promise';
2+
import {callPromise} from '../call-promise';
33
import {Account, getAddressOf} from './misc/account';
44

55
export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
@@ -9,9 +9,12 @@ export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
99
accounts: Account[],
1010
balanceChanges: BigNumberish[]
1111
) {
12-
transactionPromise(this);
12+
callPromise(this);
1313
const isNegated = this.__flags.negate === true;
14-
const derivedPromise = this.txPromise.then(async () => {
14+
const derivedPromise = this.callPromise.then(async () => {
15+
if (!('txReceipt' in this)) {
16+
throw new Error('The changeTokenBalances matcher must be called on a transaction');
17+
}
1518
const addresses = await getAddresses(accounts);
1619
const actualChanges = await getBalanceChanges(this.txReceipt, token, addresses);
1720
return [actualChanges, addresses];
@@ -32,7 +35,7 @@ export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
3235
});
3336
this.then = derivedPromise.then.bind(derivedPromise);
3437
this.catch = derivedPromise.catch.bind(derivedPromise);
35-
this.txPromise = derivedPromise;
38+
this.callPromise = derivedPromise;
3639
return this;
3740
});
3841
}

0 commit comments

Comments
 (0)