Skip to content

Commit 587ff49

Browse files
authoredMay 4, 2022
🪢 Chaining matchers (#717)
1 parent de3905f commit 587ff49

16 files changed

+581
-164
lines changed
 

‎.changeset/big-shrimps-rule.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ethereum-waffle/chai": patch
3+
---
4+
5+
Allow chaining matchers

‎docs/source/configuration.rst

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Afterwards update your :code:`package.json` build script:
4949
}
5050
5151
.. group-tab:: Waffle 2.5.0
52+
5253
.. code-block:: json
5354
5455
{

‎docs/source/migration-guides.rst

+39-3
Original file line numberDiff line numberDiff line change
@@ -436,10 +436,10 @@ In the new Ganache, you should not override the wallet config, otherwise you mig
436436
}
437437
})
438438
439-
Chaining :code:`emit` matchers
440-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
439+
Chaining matchers
440+
~~~~~~~~~~~~~~~~~
441441

442-
Now when testing events on a smart contract you can conveniently chain :code:`emit` matchers.
442+
Now when testing events on a smart contract you can conveniently chain matchers. It can be especially useful when testing events.
443443

444444
.. code-block:: ts
445445
@@ -454,3 +454,39 @@ Now when testing events on a smart contract you can conveniently chain :code:`em
454454
'Two'
455455
)
456456
.to.not.emit(contract, 'Three');
457+
458+
:code:`changeEtherBalance`, :code:`changeEtherBalances`, :code:`changeTokenbalance` and :code:`changeTokenBalances` matchers also support chaining:
459+
460+
.. code-block:: ts
461+
462+
await token.approve(complex.address, 100);
463+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
464+
await expect(tx)
465+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
466+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
467+
.and.to.emit(complex, 'TransferredEther').withArgs(200)
468+
.and.to.emit(complex, 'TransferredTokens').withArgs(100);
469+
470+
Although you may find it more convenient to write multiple expects. The test below is equivalent to the one above:
471+
472+
.. code-block:: ts
473+
474+
await token.approve(complex.address, 100);
475+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
476+
await expect(tx).to.changeTokenBalances(token, [sender, receiver], [-100, 100]);
477+
await expect(tx).to.changeEtherBalances([sender, receiver], [-200, 200]);
478+
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
479+
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
480+
481+
Note that in both cases you can use :code:`chai` negation :code:`not`. In a case of a single expect everything after :code:`not` is negated.
482+
483+
.. code-block:: ts
484+
485+
await token.approve(complex.address, 100);
486+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
487+
await expect(expect(tx)
488+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
489+
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
490+
.and.not
491+
.to.emit(complex, 'UnusedEvent') // This is negated
492+
.and.to.changeEtherBalances([sender, receiver], [-100, 100]) // This is negated as well
+22-17
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
11
import {BigNumber, BigNumberish} from 'ethers';
22
import {Account, getAddressOf} from './misc/account';
33
import {getBalanceChange} from './changeEtherBalance';
4+
import {transactionPromise} from '../transaction-promise';
45

56
export function supportChangeBalance(Assertion: Chai.AssertionStatic) {
67
Assertion.addMethod('changeBalance', function (
78
this: any,
89
account: Account,
910
balanceChange: BigNumberish
1011
) {
11-
const subject = this._obj;
12-
const derivedPromise = Promise.all([
13-
getBalanceChange(subject, account, {includeFee: true}),
14-
getAddressOf(account)
15-
]).then(
16-
([actualChange, address]) => {
17-
this.assert(
18-
actualChange.eq(BigNumber.from(balanceChange)),
19-
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
20-
`but it has changed by ${actualChange} wei`,
21-
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
22-
balanceChange,
23-
actualChange
24-
);
25-
}
26-
);
12+
transactionPromise(this);
13+
const isNegated = this.__flags.negate === true;
14+
const derivedPromise = this.txPromise.then(() => {
15+
return Promise.all([
16+
getBalanceChange(this.txResponse, account, {includeFee: true}),
17+
getAddressOf(account)
18+
]);
19+
}).then(([actualChange, address]: [BigNumber, string]) => {
20+
const isCurrentlyNegated = this.__flags.negate === true;
21+
this.__flags.negate = isNegated;
22+
this.assert(
23+
actualChange.eq(BigNumber.from(balanceChange)),
24+
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
25+
`but it has changed by ${actualChange} wei`,
26+
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
27+
balanceChange,
28+
actualChange
29+
);
30+
this.__flags.negate = isCurrentlyNegated;
31+
});
2732
this.then = derivedPromise.then.bind(derivedPromise);
2833
this.catch = derivedPromise.catch.bind(derivedPromise);
29-
this.promise = derivedPromise;
34+
this.txPromise = derivedPromise;
3035
return this;
3136
});
3237
}
+24-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {BigNumber, BigNumberish} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {getBalanceChanges} from './changeEtherBalances';
34
import {Account} from './misc/account';
45
import {getAddresses} from './misc/balance';
@@ -9,28 +10,31 @@ export function supportChangeBalances(Assertion: Chai.AssertionStatic) {
910
accounts: Account[],
1011
balanceChanges: BigNumberish[]
1112
) {
12-
const subject = this._obj;
13-
14-
const derivedPromise = Promise.all([
15-
getBalanceChanges(subject, accounts, {includeFee: true}),
16-
getAddresses(accounts)
17-
]).then(
18-
([actualChanges, accountAddresses]) => {
19-
this.assert(
20-
actualChanges.every((change, ind) =>
21-
change.eq(BigNumber.from(balanceChanges[ind]))
22-
),
23-
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
24-
`but it has changed by ${actualChanges} wei`,
25-
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
26-
balanceChanges.map((balanceChange) => balanceChange.toString()),
27-
actualChanges.map((actualChange) => actualChange.toString())
28-
);
29-
}
30-
);
13+
transactionPromise(this);
14+
const isNegated = this.__flags.negate === true;
15+
const derivedPromise = this.txPromise.then(() => {
16+
return Promise.all([
17+
getBalanceChanges(this.txResponse, accounts, {includeFee: true}),
18+
getAddresses(accounts)
19+
]);
20+
}).then(([actualChanges, accountAddresses]: [BigNumber[], string[]]) => {
21+
const isCurrentlyNegated = this.__flags.negate === true;
22+
this.__flags.negate = isNegated;
23+
this.assert(
24+
actualChanges.every((change, ind) =>
25+
change.eq(BigNumber.from(balanceChanges[ind]))
26+
),
27+
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
28+
`but it has changed by ${actualChanges} wei`,
29+
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
30+
balanceChanges.map((balanceChange) => balanceChange.toString()),
31+
actualChanges.map((actualChange) => actualChange.toString())
32+
);
33+
this.__flags.negate = isCurrentlyNegated;
34+
});
3135
this.then = derivedPromise.then.bind(derivedPromise);
3236
this.catch = derivedPromise.catch.bind(derivedPromise);
33-
this.promise = derivedPromise;
37+
this.txPromise = derivedPromise;
3438
return this;
3539
});
3640
}

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

+26-30
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {BigNumber, BigNumberish, providers} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {ensure} from './calledOnContract/utils';
34
import {Account, getAddressOf} from './misc/account';
45
import {BalanceChangeOptions} from './misc/balance';
@@ -10,53 +11,48 @@ export function supportChangeEtherBalance(Assertion: Chai.AssertionStatic) {
1011
balanceChange: BigNumberish,
1112
options: BalanceChangeOptions
1213
) {
13-
const subject = this._obj;
14-
const derivedPromise = Promise.all([
15-
getBalanceChange(subject, account, options),
16-
getAddressOf(account)
17-
]).then(
18-
([actualChange, address]) => {
19-
this.assert(
20-
actualChange.eq(BigNumber.from(balanceChange)),
21-
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
14+
transactionPromise(this);
15+
const isNegated = this.__flags.negate === true;
16+
const derivedPromise = this.txPromise.then(() => {
17+
return Promise.all([
18+
getBalanceChange(this.txResponse, account, options),
19+
getAddressOf(account)
20+
]);
21+
}).then(([actualChange, address]: [BigNumber, string]) => {
22+
const isCurrentlyNegated = this.__flags.negate === true;
23+
this.__flags.negate = isNegated;
24+
this.assert(
25+
actualChange.eq(BigNumber.from(balanceChange)),
26+
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
2227
`but it has changed by ${actualChange} wei`,
23-
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
24-
balanceChange,
25-
actualChange
26-
);
27-
}
28+
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
29+
balanceChange,
30+
actualChange
31+
);
32+
this.__flags.negate = isCurrentlyNegated;
33+
}
2834
);
2935
this.then = derivedPromise.then.bind(derivedPromise);
3036
this.catch = derivedPromise.catch.bind(derivedPromise);
31-
this.promise = derivedPromise;
37+
this.txPromise = derivedPromise;
3238
return this;
3339
});
3440
}
3541

3642
export async function getBalanceChange(
37-
transaction:
38-
| providers.TransactionResponse
39-
| (() => Promise<providers.TransactionResponse> | providers.TransactionResponse),
43+
txResponse: providers.TransactionResponse,
4044
account: Account,
4145
options?: BalanceChangeOptions
4246
) {
4347
ensure(account.provider !== undefined, TypeError, 'Provider not found');
44-
45-
let txResponse: providers.TransactionResponse;
46-
47-
if (typeof transaction === 'function') {
48-
txResponse = await transaction();
49-
} else {
50-
txResponse = transaction;
51-
}
52-
5348
const txReceipt = await txResponse.wait();
5449
const txBlockNumber = txReceipt.blockNumber;
50+
const address = await getAddressOf(account);
5551

56-
const balanceAfter = await account.provider.getBalance(getAddressOf(account), txBlockNumber);
57-
const balanceBefore = await account.provider.getBalance(getAddressOf(account), txBlockNumber - 1);
52+
const balanceAfter = await account.provider.getBalance(address, txBlockNumber);
53+
const balanceBefore = await account.provider.getBalance(address, txBlockNumber - 1);
5854

59-
if (options?.includeFee !== true && await getAddressOf(account) === txResponse.from) {
55+
if (options?.includeFee !== true && address === txReceipt.from) {
6056
const gasPrice = txResponse.gasPrice ?? txReceipt.effectiveGasPrice;
6157
const gasUsed = txReceipt.gasUsed;
6258
const txFee = gasPrice.mul(gasUsed);

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

+25-31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {BigNumber, BigNumberish, providers} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {getAddressOf, Account} from './misc/account';
34
import {BalanceChangeOptions, getAddresses, getBalances} from './misc/balance';
45

@@ -9,47 +10,40 @@ export function supportChangeEtherBalances(Assertion: Chai.AssertionStatic) {
910
balanceChanges: BigNumberish[],
1011
options: BalanceChangeOptions
1112
) {
12-
const subject = this._obj;
13-
14-
const derivedPromise = Promise.all([
15-
getBalanceChanges(subject, accounts, options),
16-
getAddresses(accounts)
17-
]).then(
18-
([actualChanges, accountAddresses]) => {
19-
this.assert(
20-
actualChanges.every((change, ind) =>
21-
change.eq(BigNumber.from(balanceChanges[ind]))
22-
),
23-
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
24-
`but it has changed by ${actualChanges} wei`,
25-
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
26-
balanceChanges.map((balanceChange) => balanceChange.toString()),
27-
actualChanges.map((actualChange) => actualChange.toString())
28-
);
29-
}
30-
);
13+
transactionPromise(this);
14+
const isNegated = this.__flags.negate === true;
15+
const derivedPromise = this.txPromise.then(() => {
16+
return Promise.all([
17+
getBalanceChanges(this.txResponse, accounts, options),
18+
getAddresses(accounts)
19+
]);
20+
}).then(([actualChanges, accountAddresses]: [BigNumber[], string[]]) => {
21+
const isCurrentlyNegated = this.__flags.negate === true;
22+
this.__flags.negate = isNegated;
23+
this.assert(
24+
actualChanges.every((change, ind) =>
25+
change.eq(BigNumber.from(balanceChanges[ind]))
26+
),
27+
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
28+
`but it has changed by ${actualChanges} wei`,
29+
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
30+
balanceChanges.map((balanceChange) => balanceChange.toString()),
31+
actualChanges.map((actualChange) => actualChange.toString())
32+
);
33+
this.__flags.negate = isCurrentlyNegated;
34+
});
3135
this.then = derivedPromise.then.bind(derivedPromise);
3236
this.catch = derivedPromise.catch.bind(derivedPromise);
33-
this.promise = derivedPromise;
37+
this.txPromise = derivedPromise;
3438
return this;
3539
});
3640
}
3741

3842
export async function getBalanceChanges(
39-
transaction:
40-
| providers.TransactionResponse
41-
| (() => Promise<providers.TransactionResponse> | providers.TransactionResponse),
43+
txResponse: providers.TransactionResponse,
4244
accounts: Account[],
4345
options: BalanceChangeOptions
4446
) {
45-
let txResponse: providers.TransactionResponse;
46-
47-
if (typeof transaction === 'function') {
48-
txResponse = await transaction();
49-
} else {
50-
txResponse = transaction;
51-
}
52-
5347
const txReceipt = await txResponse.wait();
5448
const txBlockNumber = txReceipt.blockNumber;
5549

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

45
export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
@@ -8,37 +9,47 @@ export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) {
89
account: Account,
910
balanceChange: BigNumberish
1011
) {
11-
const subject = this._obj;
12-
const derivedPromise = Promise.all([
13-
getBalanceChange(subject, token, account),
14-
getAddressOf(account)
15-
]).then(
16-
([actualChange, address]) => {
17-
this.assert(
18-
actualChange.eq(BigNumber.from(balanceChange)),
19-
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
12+
transactionPromise(this);
13+
const isNegated = this.__flags.negate === true;
14+
const derivedPromise = this.txPromise.then(async () => {
15+
const address = await getAddressOf(account);
16+
const actualChanges = await getBalanceChange(this.txReceipt, token, address);
17+
return [actualChanges, address];
18+
}).then(([actualChange, address]: [BigNumber, string]) => {
19+
const isCurrentlyNegated = this.__flags.negate === true;
20+
this.__flags.negate = isNegated;
21+
this.assert(
22+
actualChange.eq(BigNumber.from(balanceChange)),
23+
`Expected "${address}" to change balance by ${balanceChange} wei, ` +
2024
`but it has changed by ${actualChange} wei`,
21-
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
22-
balanceChange,
23-
actualChange
24-
);
25-
}
26-
);
25+
`Expected "${address}" to not change balance by ${balanceChange} wei,`,
26+
balanceChange,
27+
actualChange
28+
);
29+
this.__flags.negate = isCurrentlyNegated;
30+
});
2731
this.then = derivedPromise.then.bind(derivedPromise);
2832
this.catch = derivedPromise.catch.bind(derivedPromise);
29-
this.promise = derivedPromise;
33+
this.txPromise = derivedPromise;
3034
return this;
3135
});
3236
}
3337

3438
async function getBalanceChange(
35-
transactionCall: (() => Promise<void> | void),
39+
txReceipt: providers.TransactionReceipt,
3640
token: Contract,
37-
account: Account
41+
address: string
3842
) {
39-
const balanceBefore: BigNumber = await token['balanceOf(address)'](await getAddressOf(account));
40-
await transactionCall();
41-
const balanceAfter: BigNumber = await token['balanceOf(address)'](await getAddressOf(account));
43+
const txBlockNumber = txReceipt.blockNumber;
44+
45+
const balanceBefore: BigNumber = await token['balanceOf(address)'](
46+
address,
47+
{blockTag: txBlockNumber - 1}
48+
);
49+
const balanceAfter: BigNumber = await token['balanceOf(address)'](
50+
address,
51+
{blockTag: txBlockNumber}
52+
);
4253

4354
return balanceAfter.sub(balanceBefore);
4455
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {BigNumber, BigNumberish, Contract} from 'ethers';
1+
import {BigNumber, BigNumberish, Contract, providers} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {Account, getAddressOf} from './misc/account';
34

45
export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
@@ -8,27 +9,30 @@ export function supportChangeTokenBalances(Assertion: Chai.AssertionStatic) {
89
accounts: Account[],
910
balanceChanges: BigNumberish[]
1011
) {
11-
const subject = this._obj;
12-
const derivedPromise = Promise.all([
13-
getBalanceChanges(subject, token, accounts),
14-
getAddresses(accounts)
15-
]).then(
16-
([actualChanges, accountAddresses]) => {
17-
this.assert(
18-
actualChanges.every((change, ind) =>
19-
change.eq(BigNumber.from(balanceChanges[ind]))
20-
),
21-
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
22-
`but it has changed by ${actualChanges} wei`,
23-
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
24-
balanceChanges.map((balanceChange) => balanceChange.toString()),
25-
actualChanges.map((actualChange) => actualChange.toString())
26-
);
27-
}
28-
);
12+
transactionPromise(this);
13+
const isNegated = this.__flags.negate === true;
14+
const derivedPromise = this.txPromise.then(async () => {
15+
const addresses = await getAddresses(accounts);
16+
const actualChanges = await getBalanceChanges(this.txReceipt, token, addresses);
17+
return [actualChanges, addresses];
18+
}).then(([actualChanges, accountAddresses]: [BigNumber[], string[]]) => {
19+
const isCurrentlyNegated = this.__flags.negate === true;
20+
this.__flags.negate = isNegated;
21+
this.assert(
22+
actualChanges.every((change, ind) =>
23+
change.eq(BigNumber.from(balanceChanges[ind]))
24+
),
25+
`Expected ${accountAddresses} to change balance by ${balanceChanges} wei, ` +
26+
`but it has changed by ${actualChanges} wei`,
27+
`Expected ${accountAddresses} to not change balance by ${balanceChanges} wei,`,
28+
balanceChanges.map((balanceChange) => balanceChange.toString()),
29+
actualChanges.map((actualChange) => actualChange.toString())
30+
);
31+
this.__flags.negate = isCurrentlyNegated;
32+
});
2933
this.then = derivedPromise.then.bind(derivedPromise);
3034
this.catch = derivedPromise.catch.bind(derivedPromise);
31-
this.promise = derivedPromise;
35+
this.txPromise = derivedPromise;
3236
return this;
3337
});
3438
}
@@ -37,22 +41,23 @@ function getAddresses(accounts: Account[]) {
3741
return Promise.all(accounts.map((account) => getAddressOf(account)));
3842
}
3943

40-
async function getBalances(token: Contract, accounts: Account[]) {
44+
async function getBalances(token: Contract, addresses: string[], blockNumber: number) {
4145
return Promise.all(
42-
accounts.map(async (account) => {
43-
return token['balanceOf(address)'](getAddressOf(account));
46+
addresses.map((address) => {
47+
return token['balanceOf(address)'](address, {blockTag: blockNumber});
4448
})
4549
);
4650
}
4751

4852
async function getBalanceChanges(
49-
transactionCall: (() => Promise<void> | void),
53+
txReceipt: providers.TransactionReceipt,
5054
token: Contract,
51-
accounts: Account[]
55+
addresses: string[]
5256
) {
53-
const balancesBefore = await getBalances(token, accounts);
54-
await transactionCall();
55-
const balancesAfter = await getBalances(token, accounts);
57+
const txBlockNumber = txReceipt.blockNumber;
58+
59+
const balancesBefore = await getBalances(token, addresses, txBlockNumber - 1);
60+
const balancesAfter = await getBalances(token, addresses, txBlockNumber);
5661

5762
return balancesAfter.map((balance, ind) => balance.sub(balancesBefore[ind]));
5863
}

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

+22-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Contract, providers, utils} from 'ethers';
2+
import {transactionPromise} from '../transaction-promise';
23
import {waitForPendingTransaction} from './misc/transaction';
34

45
export function supportEmit(Assertion: Chai.AssertionStatic) {
@@ -7,15 +8,19 @@ export function supportEmit(Assertion: Chai.AssertionStatic) {
78
.filter((log) => log.address && log.address.toLowerCase() === contractAddress.toLowerCase());
89

910
Assertion.addMethod('emit', function (this: any, contract: Contract, eventName: string) {
10-
const tx = this._obj;
11-
const isNegated = this.__flags.negate === true;
12-
if (!('promise' in this)) {
13-
this.promise = waitForPendingTransaction(tx, contract.provider)
14-
.then((receipt) => { this.receipt = receipt; });
11+
if (typeof this._obj === 'string') {
12+
// Handle specific case of using transaction hash to specify transaction. Done for backwards compatibility.
13+
this.txPromise = waitForPendingTransaction(this._obj, contract.provider)
14+
.then(txReceipt => {
15+
this.txReceipt = txReceipt;
16+
});
17+
} else {
18+
transactionPromise(this);
1519
}
16-
this.promise = this.promise
20+
const isNegated = this.__flags.negate === true;
21+
this.txPromise = this.txPromise
1722
.then(() => {
18-
const receipt: providers.TransactionReceipt = this.receipt;
23+
const receipt: providers.TransactionReceipt = this.txReceipt;
1924
let eventFragment: utils.EventFragment | undefined;
2025
try {
2126
eventFragment = contract.interface.getEvent(eventName);
@@ -52,8 +57,8 @@ export function supportEmit(Assertion: Chai.AssertionStatic) {
5257
);
5358
this.__flags.negate = isCurrentlyNegated;
5459
});
55-
this.then = this.promise.then.bind(this.promise);
56-
this.catch = this.promise.catch.bind(this.promise);
60+
this.then = this.txPromise.then.bind(this.txPromise);
61+
this.catch = this.txPromise.catch.bind(this.txPromise);
5762
this.contract = contract;
5863
this.eventName = eventName;
5964
return this;
@@ -103,14 +108,18 @@ export function supportEmit(Assertion: Chai.AssertionStatic) {
103108
};
104109

105110
Assertion.addMethod('withArgs', function (this: any, ...expectedArgs: any[]) {
106-
if (!('promise' in this)) {
111+
if (!('txPromise' in this)) {
107112
throw new Error('withArgs() must be used after emit()');
108113
}
109-
this.promise = this.promise.then(() => {
114+
const isNegated = this.__flags.negate === true;
115+
this.txPromise = this.txPromise.then(() => {
116+
const isCurrentlyNegated = this.__flags.negate === true;
117+
this.__flags.negate = isNegated;
110118
tryAssertArgsArraysEqual(this, expectedArgs, this.logs);
119+
this.__flags.negate = isCurrentlyNegated;
111120
});
112-
this.then = this.promise.then.bind(this.promise);
113-
this.catch = this.promise.catch.bind(this.promise);
121+
this.then = this.txPromise.then.bind(this.txPromise);
122+
this.catch = this.txPromise.catch.bind(this.txPromise);
114123
return this;
115124
});
116125
}
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {providers} from 'ethers';
2+
3+
type TransactionResponse = providers.TransactionResponse;
4+
type MaybePromise<T> = T | Promise<T>;
5+
6+
/**
7+
* Takes a chai object (usually a `this` object) and adds a `promise` property to it.
8+
* Adds a `response` property to the chai object with the transaction response.
9+
* The promised is resolved when the transaction is mined.
10+
* Adds a `receipt` property to the chai object with the transaction receipt when the promise is resolved.
11+
* May be called on a chai object which contains any of these:
12+
* - a transaction response
13+
* - a promise which resolves to a transaction response
14+
* - a function that returns a transaction response
15+
* - a function that returns a promise which resolves to a transaction response
16+
*/
17+
export const transactionPromise = (chaiObj: any) => {
18+
if ('txPromise' in chaiObj) {
19+
return;
20+
}
21+
22+
const tx: (() => MaybePromise<TransactionResponse>) | MaybePromise<TransactionResponse> = chaiObj._obj;
23+
let txResponse: MaybePromise<TransactionResponse>;
24+
25+
if (typeof tx === 'function') {
26+
txResponse = tx();
27+
} else {
28+
txResponse = tx;
29+
}
30+
31+
if (!('then' in txResponse)) {
32+
chaiObj.txResponse = txResponse;
33+
chaiObj.txPromise = txResponse.wait().then(txReceipt => {
34+
chaiObj.txReceipt = txReceipt;
35+
});
36+
} else {
37+
chaiObj.txPromise = txResponse.then(async txResponse => {
38+
chaiObj.txResponse = txResponse;
39+
const txReceipt = await txResponse.wait();
40+
chaiObj.txReceipt = txReceipt;
41+
});
42+
}
43+
44+
// Setting `then` and `catch` on the chai object to be compliant with the chai-aspromised library.
45+
chaiObj.then = chaiObj.txPromise.then.bind(chaiObj.txPromise);
46+
chaiObj.catch = chaiObj.txPromise.catch.bind(chaiObj.txPromise);
47+
};
+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
export const COMPLEX_SOURCE = `
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
pragma solidity =0.7.0;
5+
6+
import {ERC20} from './MockToken.sol';
7+
8+
contract Complex {
9+
ERC20 public token;
10+
11+
event TransferredEther(uint value);
12+
event TransferredTokens(uint value);
13+
event UnusedEvent(uint value);
14+
15+
constructor(ERC20 _token) {
16+
token = _token;
17+
}
18+
19+
function doEverything(address payable receiver, uint tokens) public payable {
20+
receiver.transfer(msg.value);
21+
token.transferFrom(msg.sender, receiver, tokens);
22+
emit TransferredEther(msg.value);
23+
emit TransferredTokens(tokens);
24+
}
25+
}
26+
`;
27+
28+
export const COMPLEX_ABI = [
29+
{
30+
inputs: [
31+
{
32+
internalType: 'contract ERC20',
33+
name: '_token',
34+
type: 'address'
35+
}
36+
],
37+
stateMutability: 'nonpayable',
38+
type: 'constructor'
39+
},
40+
{
41+
anonymous: false,
42+
inputs: [
43+
{
44+
indexed: false,
45+
internalType: 'uint256',
46+
name: 'value',
47+
type: 'uint256'
48+
}
49+
],
50+
name: 'TransferredEther',
51+
type: 'event'
52+
},
53+
{
54+
anonymous: false,
55+
inputs: [
56+
{
57+
indexed: false,
58+
internalType: 'uint256',
59+
name: 'value',
60+
type: 'uint256'
61+
}
62+
],
63+
name: 'TransferredTokens',
64+
type: 'event'
65+
},
66+
{
67+
anonymous: false,
68+
inputs: [
69+
{
70+
indexed: false,
71+
internalType: 'uint256',
72+
name: 'value',
73+
type: 'uint256'
74+
}
75+
],
76+
name: 'UnusedEvent',
77+
type: 'event'
78+
},
79+
{
80+
inputs: [
81+
{
82+
internalType: 'address payable',
83+
name: 'receiver',
84+
type: 'address'
85+
},
86+
{
87+
internalType: 'uint256',
88+
name: 'tokens',
89+
type: 'uint256'
90+
}
91+
],
92+
name: 'doEverything',
93+
outputs: [],
94+
stateMutability: 'payable',
95+
type: 'function'
96+
},
97+
{
98+
inputs: [],
99+
name: 'token',
100+
outputs: [
101+
{
102+
internalType: 'contract ERC20',
103+
name: '',
104+
type: 'address'
105+
}
106+
],
107+
stateMutability: 'view',
108+
type: 'function'
109+
}
110+
];
111+
112+
// eslint-disable-next-line max-len
113+
export const COMPLEX_BYTECODE = '608060405234801561001057600080fd5b5060405161034f38038061034f8339818101604052602081101561003357600080fd5b8101908080519060200190929190505050806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550506102bb806100946000396000f3fe6080604052600436106100295760003560e01c806320c111991461002e578063fc0c546a1461007c575b600080fd5b61007a6004803603604081101561004457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506100bd565b005b34801561008857600080fd5b50610091610261565b604051808273ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b8173ffffffffffffffffffffffffffffffffffffffff166108fc349081150290604051600060405180830381858888f19350505050158015610103573d6000803e3d6000fd5b5060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166323b872dd3384846040518463ffffffff1660e01b8152600401808473ffffffffffffffffffffffffffffffffffffffff1681526020018373ffffffffffffffffffffffffffffffffffffffff1681526020018281526020019350505050602060405180830381600087803b1580156101b357600080fd5b505af11580156101c7573d6000803e3d6000fd5b505050506040513d60208110156101dd57600080fd5b8101908080519060200190929190505050507f0236f81eab444b36009ff6ba3d30740fc6bff318c418412263fbfbada8c57c09346040518082815260200191505060405180910390a17f1e4f6f9cdba2cbf50c993296a58398e79fdb4e5dd0ca4a6a8c93e0d1b389735f816040518082815260200191505060405180910390a15050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff168156fea2646970667358221220d9014649ad333894164ee0481f6183ea0587e3c41c95d32ba62599d5be36a36364736f6c63430007000033';

‎waffle-chai/test/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ chai.use(waffleChai);
88
export {calledOnContractTest} from './matchers/calledOnContract/calledOnContractTest';
99
export {calledOnContractValidatorsTest} from './matchers/calledOnContract/calledOnContractValidatorsTest';
1010
export {calledOnContractWithTest} from './matchers/calledOnContract/calledOnContractWithTest';
11+
export {chainingMatchersTest} from './matchers/chainingTest';
1112
export {changeEtherBalanceTest} from './matchers/changeEtherBalanceTest';
1213
export {changeEtherBalancesTest} from './matchers/changeEtherBalancesTest';
1314
export {changeTokenBalanceTest} from './matchers/changeTokenBalanceTest';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {chainingMatchersTest} from './chainingTest';
2+
import {describeMockProviderCases} from './MockProviderCases';
3+
4+
describeMockProviderCases('INTEGRATION: chaining matchers', (provider) => {
5+
chainingMatchersTest(provider);
6+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import {AssertionError, expect} from 'chai';
2+
import {Wallet, Contract, ContractFactory} from 'ethers';
3+
import {MOCK_TOKEN_ABI, MOCK_TOKEN_BYTECODE} from '../contracts/MockToken';
4+
import {COMPLEX_ABI, COMPLEX_BYTECODE} from '../contracts/Complex';
5+
6+
import {MockProvider} from '@ethereum-waffle/provider';
7+
8+
export const chainingMatchersTest = (provider: MockProvider) => {
9+
let sender: Wallet;
10+
let receiver: Wallet;
11+
let token: Contract;
12+
let tokenFactory: ContractFactory;
13+
let complex: Contract;
14+
let complexFactory: ContractFactory;
15+
16+
before(async () => {
17+
const wallets = provider.getWallets();
18+
sender = wallets[0];
19+
receiver = wallets[1];
20+
tokenFactory = new ContractFactory(MOCK_TOKEN_ABI, MOCK_TOKEN_BYTECODE, sender);
21+
token = await tokenFactory.deploy('MockToken', 'Mock', 18, 1000000000);
22+
complexFactory = new ContractFactory(COMPLEX_ABI, COMPLEX_BYTECODE, sender);
23+
complex = await complexFactory.deploy(token.address);
24+
});
25+
26+
it('Balances different calls', async () => {
27+
await token.approve(complex.address, 100);
28+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
29+
await expect(tx).to.changeTokenBalances(token, [sender, receiver], [-100, 100]);
30+
await expect(tx).to.changeEtherBalances([sender, receiver], [-200, 200]);
31+
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
32+
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
33+
});
34+
35+
it('Balances different calls some fail', async () => {
36+
await token.approve(complex.address, 100);
37+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
38+
await expect(
39+
expect(tx).to.changeTokenBalances(token, [sender, receiver], [-101, 101])
40+
).to.be.eventually.rejectedWith(
41+
AssertionError,
42+
`Expected ${sender.address},${receiver.address} ` +
43+
'to change balance by -101,101 wei, but it has changed by -100,100 wei'
44+
);
45+
await expect(tx).to.changeEtherBalances([sender, receiver], [-200, 200]);
46+
await expect(
47+
expect(tx).to.emit(complex, 'TransferredEther').withArgs(201)
48+
).to.be.eventually.rejectedWith(
49+
AssertionError,
50+
'Expected "200" to be equal 201'
51+
);
52+
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
53+
});
54+
55+
it('Balance different calls', async () => {
56+
await token.approve(complex.address, 100);
57+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
58+
await expect(tx).to.changeTokenBalance(token, sender, -100);
59+
await expect(tx).to.changeTokenBalance(token, receiver, 100);
60+
await expect(tx).to.changeEtherBalance(sender, -200);
61+
await expect(tx).to.changeEtherBalance(receiver, 200);
62+
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
63+
await expect(tx).to.emit(complex, 'TransferredTokens').withArgs(100);
64+
});
65+
66+
it('Balance different calls some fail', async () => {
67+
await token.approve(complex.address, 100);
68+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
69+
await expect(
70+
expect(tx).to.changeTokenBalance(token, sender, -101)
71+
).to.be.eventually.rejectedWith(
72+
AssertionError,
73+
`Expected "${sender.address}" to change balance by -101 wei, but it has changed by -100 wei`
74+
);
75+
await expect(tx).to.changeTokenBalance(token, receiver, 100);
76+
await expect(tx).to.changeEtherBalance(sender, -200);
77+
await expect(
78+
expect(tx).to.changeEtherBalance(receiver, 201)
79+
).to.be.eventually.rejectedWith(
80+
AssertionError,
81+
`Expected "${receiver.address}" to change balance by 201 wei, but it has changed by 200 wei`
82+
);
83+
await expect(tx).to.emit(complex, 'TransferredEther').withArgs(200);
84+
await expect(
85+
expect(tx).not.to.emit(complex, 'TransferredTokens')
86+
).to.be.eventually.rejectedWith(
87+
AssertionError,
88+
'Expected event "TransferredTokens" NOT to be emitted, but it was'
89+
);
90+
});
91+
92+
it('Balances one call', async () => {
93+
await token.approve(complex.address, 100);
94+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
95+
await expect(tx)
96+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
97+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
98+
.and.to.emit(complex, 'TransferredEther').withArgs(200)
99+
.and.to.emit(complex, 'TransferredTokens').withArgs(100);
100+
});
101+
102+
it('Balances one call first fail', async () => {
103+
await token.approve(complex.address, 100);
104+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
105+
await expect(expect(tx)
106+
.to.changeTokenBalances(token, [sender, receiver], [-100, 101])
107+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
108+
.and.to.emit(complex, 'TransferredEther').withArgs(200)
109+
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
110+
).to.be.eventually.rejectedWith(
111+
AssertionError,
112+
`Expected ${sender.address},${receiver.address} ` +
113+
'to change balance by -100,101 wei, but it has changed by -100,100 wei'
114+
);
115+
});
116+
117+
it('Balances one call third fail', async () => {
118+
await token.approve(complex.address, 100);
119+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
120+
await expect(expect(tx)
121+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
122+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
123+
.and.to.emit(complex, 'TransferredEther').withArgs(199)
124+
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
125+
).to.be.eventually.rejectedWith(
126+
AssertionError,
127+
'Expected "200" to be equal 199'
128+
);
129+
});
130+
131+
it('Balances not to emit', async () => {
132+
await token.approve(complex.address, 100);
133+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
134+
await expect(tx)
135+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
136+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
137+
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
138+
.and.to.emit(complex, 'TransferredEther').withArgs(200)
139+
.and.not.to.emit(complex, 'UnusedEvent');
140+
});
141+
142+
it('Balances not to emit fail', async () => {
143+
await token.approve(complex.address, 100);
144+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
145+
await expect(expect(tx)
146+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
147+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
148+
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
149+
.and.not.to.emit(complex, 'TransferredEther')
150+
).to.be.eventually.rejectedWith(
151+
AssertionError,
152+
'Expected event "TransferredEther" NOT to be emitted, but it was'
153+
);
154+
});
155+
156+
it('Balances not to change fail', async () => {
157+
await token.approve(complex.address, 100);
158+
const tx = await complex.doEverything(receiver.address, 100, {value: 200});
159+
await expect(expect(tx)
160+
.to.changeTokenBalances(token, [sender, receiver], [-100, 100])
161+
.and.to.emit(complex, 'TransferredTokens').withArgs(100)
162+
.and.not
163+
.to.emit(complex, 'UnusedEvent')
164+
.and.to.changeEtherBalances([sender, receiver], [-200, 200])
165+
).to.be.eventually.rejectedWith(
166+
AssertionError,
167+
`Expected ${sender.address},${receiver.address} ` +
168+
'to not change balance by -200,200 wei'
169+
);
170+
});
171+
};
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {waffle} from 'hardhat';
2+
import {MockProvider} from 'ethereum-waffle';
3+
import {chainingMatchersTest} from '@ethereum-waffle/chai/test';
4+
5+
describe('INTEGRATION: chaining', () => {
6+
const provider = waffle.provider as MockProvider;
7+
8+
before(async () => {
9+
await provider.send('hardhat_reset', []);
10+
});
11+
12+
chainingMatchersTest(provider);
13+
});

0 commit comments

Comments
 (0)
Please sign in to comment.