Skip to content

Commit ac55f40

Browse files
SevenSwenschaable
authored andcommitted
Accept predicate functions in balance and token balance change matchers
1 parent af7807c commit ac55f40

8 files changed

+365
-74
lines changed

.changeset/strange-ladybugs-end.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nomicfoundation/hardhat-chai-matchers": patch
3+
---
4+
5+
Accept predicate functions in the `changeEtherBalance`, `changeEthersBalances`, `changeTokenBalance` and `changeTokenBalances` matchers (thanks @SevenSwen and @k06a!)

packages/hardhat-chai-matchers/src/internal/changeEtherBalance.ts

+15-8
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function supportChangeEtherBalance(
2121
function (
2222
this: any,
2323
account: Addressable | string,
24-
balanceChange: BigNumberish,
24+
balanceChange: BigNumberish | ((change: bigint) => boolean),
2525
options?: BalanceChangeOptions
2626
) {
2727
const { toBigInt } = require("ethers") as typeof EthersT;
@@ -41,13 +41,20 @@ export function supportChangeEtherBalance(
4141
]) => {
4242
const assert = buildAssert(negated, checkBalanceChange);
4343

44-
const expectedChange = toBigInt(balanceChange);
45-
46-
assert(
47-
actualChange === expectedChange,
48-
`Expected the ether balance of "${address}" to change by ${balanceChange.toString()} wei, but it changed by ${actualChange.toString()} wei`,
49-
`Expected the ether balance of "${address}" NOT to change by ${balanceChange.toString()} wei, but it did`
50-
);
44+
if (typeof balanceChange === "function") {
45+
assert(
46+
balanceChange(actualChange),
47+
`Expected the ether balance change of "${address}" to satisfy the predicate, but it didn't (balance change: ${actualChange.toString()} wei)`,
48+
`Expected the ether balance change of "${address}" to NOT satisfy the predicate, but it did (balance change: ${actualChange.toString()} wei)`
49+
);
50+
} else {
51+
const expectedChange = toBigInt(balanceChange);
52+
assert(
53+
actualChange === expectedChange,
54+
`Expected the ether balance of "${address}" to change by ${balanceChange.toString()} wei, but it changed by ${actualChange.toString()} wei`,
55+
`Expected the ether balance of "${address}" NOT to change by ${balanceChange.toString()} wei, but it did`
56+
);
57+
}
5158
};
5259

5360
const derivedPromise = Promise.all([

packages/hardhat-chai-matchers/src/internal/changeEtherBalances.ts

+57-40
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function supportChangeEtherBalances(
1818
function (
1919
this: any,
2020
accounts: Array<Addressable | string>,
21-
balanceChanges: BigNumberish[],
21+
balanceChanges: BigNumberish[] | ((changes: bigint[]) => boolean),
2222
options?: BalanceChangeOptions
2323
) {
2424
const { toBigInt } = require("ethers") as typeof EthersT;
@@ -37,51 +37,68 @@ export function supportChangeEtherBalances(
3737
chaiUtils
3838
);
3939

40+
if (
41+
Array.isArray(balanceChanges) &&
42+
accounts.length !== balanceChanges.length
43+
) {
44+
throw new Error(
45+
`The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})`
46+
);
47+
}
48+
4049
const checkBalanceChanges = ([actualChanges, accountAddresses]: [
4150
bigint[],
4251
string[]
4352
]) => {
4453
const assert = buildAssert(negated, checkBalanceChanges);
4554

46-
assert(
47-
actualChanges.every(
48-
(change, ind) => change === toBigInt(balanceChanges[ind])
49-
),
50-
() => {
51-
const lines: string[] = [];
52-
actualChanges.forEach((change: bigint, i) => {
53-
if (change !== toBigInt(balanceChanges[i])) {
54-
lines.push(
55-
`Expected the ether balance of ${
56-
accountAddresses[i]
57-
} (the ${ordinal(
58-
i + 1
59-
)} address in the list) to change by ${balanceChanges[
60-
i
61-
].toString()} wei, but it changed by ${change.toString()} wei`
62-
);
63-
}
64-
});
65-
return lines.join("\n");
66-
},
67-
() => {
68-
const lines: string[] = [];
69-
actualChanges.forEach((change: bigint, i) => {
70-
if (change === toBigInt(balanceChanges[i])) {
71-
lines.push(
72-
`Expected the ether balance of ${
73-
accountAddresses[i]
74-
} (the ${ordinal(
75-
i + 1
76-
)} address in the list) NOT to change by ${balanceChanges[
77-
i
78-
].toString()} wei, but it did`
79-
);
80-
}
81-
});
82-
return lines.join("\n");
83-
}
84-
);
55+
if (typeof balanceChanges === "function") {
56+
assert(
57+
balanceChanges(actualChanges),
58+
"Expected the balance changes of the accounts to satisfy the predicate, but they didn't",
59+
"Expected the balance changes of the accounts to NOT satisfy the predicate, but they did"
60+
);
61+
} else {
62+
assert(
63+
actualChanges.every(
64+
(change, ind) => change === toBigInt(balanceChanges[ind])
65+
),
66+
() => {
67+
const lines: string[] = [];
68+
actualChanges.forEach((change: bigint, i) => {
69+
if (change !== toBigInt(balanceChanges[i])) {
70+
lines.push(
71+
`Expected the ether balance of ${
72+
accountAddresses[i]
73+
} (the ${ordinal(
74+
i + 1
75+
)} address in the list) to change by ${balanceChanges[
76+
i
77+
].toString()} wei, but it changed by ${change.toString()} wei`
78+
);
79+
}
80+
});
81+
return lines.join("\n");
82+
},
83+
() => {
84+
const lines: string[] = [];
85+
actualChanges.forEach((change: bigint, i) => {
86+
if (change === toBigInt(balanceChanges[i])) {
87+
lines.push(
88+
`Expected the ether balance of ${
89+
accountAddresses[i]
90+
} (the ${ordinal(
91+
i + 1
92+
)} address in the list) NOT to change by ${balanceChanges[
93+
i
94+
].toString()} wei, but it did`
95+
);
96+
}
97+
});
98+
return lines.join("\n");
99+
}
100+
);
101+
}
85102
};
86103

87104
const derivedPromise = Promise.all([

packages/hardhat-chai-matchers/src/internal/changeTokenBalance.ts

+43-24
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function supportChangeTokenBalance(
3939
this: any,
4040
token: Token,
4141
account: Addressable | string,
42-
balanceChange: EthersT.BigNumberish
42+
balanceChange: EthersT.BigNumberish | ((change: bigint) => boolean)
4343
) {
4444
const ethers = require("ethers") as typeof EthersT;
4545

@@ -66,11 +66,19 @@ export function supportChangeTokenBalance(
6666
]) => {
6767
const assert = buildAssert(negated, checkBalanceChange);
6868

69-
assert(
70-
actualChange === ethers.toBigInt(balanceChange),
71-
`Expected the balance of ${tokenDescription} tokens for "${address}" to change by ${balanceChange.toString()}, but it changed by ${actualChange.toString()}`,
72-
`Expected the balance of ${tokenDescription} tokens for "${address}" NOT to change by ${balanceChange.toString()}, but it did`
73-
);
69+
if (typeof balanceChange === "function") {
70+
assert(
71+
balanceChange(actualChange),
72+
`Expected the balance of ${tokenDescription} tokens for "${address}" to satisfy the predicate, but it didn't (change by: ${actualChange.toString()} wei)`,
73+
`Expected the balance of ${tokenDescription} tokens for "${address}" to NOT satisfy the predicate, but it did (change by: ${actualChange.toString()} wei)`
74+
);
75+
} else {
76+
assert(
77+
actualChange === ethers.toBigInt(balanceChange),
78+
`Expected the balance of ${tokenDescription} tokens for "${address}" to change by ${balanceChange.toString()}, but it changed by ${actualChange.toString()}`,
79+
`Expected the balance of ${tokenDescription} tokens for "${address}" NOT to change by ${balanceChange.toString()}, but it did`
80+
);
81+
}
7482
};
7583

7684
const derivedPromise = Promise.all([
@@ -92,7 +100,7 @@ export function supportChangeTokenBalance(
92100
this: any,
93101
token: Token,
94102
accounts: Array<Addressable | string>,
95-
balanceChanges: EthersT.BigNumberish[]
103+
balanceChanges: EthersT.BigNumberish[] | ((changes: bigint[]) => boolean)
96104
) {
97105
const ethers = require("ethers") as typeof EthersT;
98106

@@ -124,21 +132,29 @@ export function supportChangeTokenBalance(
124132
]: [bigint[], string[], string]) => {
125133
const assert = buildAssert(negated, checkBalanceChanges);
126134

127-
assert(
128-
actualChanges.every(
129-
(change, ind) => change === ethers.toBigInt(balanceChanges[ind])
130-
),
131-
`Expected the balances of ${tokenDescription} tokens for ${
132-
addresses as any
133-
} to change by ${
134-
balanceChanges as any
135-
}, respectively, but they changed by ${actualChanges as any}`,
136-
`Expected the balances of ${tokenDescription} tokens for ${
137-
addresses as any
138-
} NOT to change by ${
139-
balanceChanges as any
140-
}, respectively, but they did`
141-
);
135+
if (typeof balanceChanges === "function") {
136+
assert(
137+
balanceChanges(actualChanges),
138+
`Expected the balance changes of ${tokenDescription} to satisfy the predicate, but they didn't`,
139+
`Expected the balance changes of ${tokenDescription} to NOT satisfy the predicate, but they did`
140+
);
141+
} else {
142+
assert(
143+
actualChanges.every(
144+
(change, ind) => change === ethers.toBigInt(balanceChanges[ind])
145+
),
146+
`Expected the balances of ${tokenDescription} tokens for ${
147+
addresses as any
148+
} to change by ${
149+
balanceChanges as any
150+
}, respectively, but they changed by ${actualChanges as any}`,
151+
`Expected the balances of ${tokenDescription} tokens for ${
152+
addresses as any
153+
} NOT to change by ${
154+
balanceChanges as any
155+
}, respectively, but they did`
156+
);
157+
}
142158
};
143159

144160
const derivedPromise = Promise.all([
@@ -159,12 +175,15 @@ function validateInput(
159175
obj: any,
160176
token: Token,
161177
accounts: Array<Addressable | string>,
162-
balanceChanges: EthersT.BigNumberish[]
178+
balanceChanges: EthersT.BigNumberish[] | ((changes: bigint[]) => boolean)
163179
) {
164180
try {
165181
checkToken(token, CHANGE_TOKEN_BALANCES_MATCHER);
166182

167-
if (accounts.length !== balanceChanges.length) {
183+
if (
184+
Array.isArray(balanceChanges) &&
185+
accounts.length !== balanceChanges.length
186+
) {
168187
throw new Error(
169188
`The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})`
170189
);

packages/hardhat-chai-matchers/src/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ declare namespace Chai {
2424
): AsyncAssertion;
2525
changeEtherBalances(
2626
accounts: any[],
27-
balances: any[],
27+
balances: any[] | ((changes: any[]) => boolean),
2828
options?: any
2929
): AsyncAssertion;
3030
changeTokenBalance(token: any, account: any, balance: any): AsyncAssertion;
3131
changeTokenBalances(
3232
token: any,
3333
account: any[],
34-
balance: any[]
34+
balance: any[] | ((changes: any[]) => boolean)
3535
): AsyncAssertion;
3636
}
3737

packages/hardhat-chai-matchers/test/changeEtherBalance.ts

+74
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ describe("INTEGRATION: changeEtherBalance matcher", function () {
107107
).to.changeEtherBalance(sender, BigInt("-200"));
108108
});
109109

110+
it("Should pass when given a predicate", async () => {
111+
await expect(() =>
112+
sender.sendTransaction({
113+
to: receiver.address,
114+
value: 200,
115+
})
116+
).to.changeEtherBalance(
117+
sender,
118+
(diff: bigint) => diff === BigInt("-200")
119+
);
120+
});
121+
110122
it("Should pass when expected balance change is passed as int and is equal to an actual", async () => {
111123
await expect(() =>
112124
sender.sendTransaction({
@@ -128,6 +140,22 @@ describe("INTEGRATION: changeEtherBalance matcher", function () {
128140
});
129141
});
130142

143+
it("Should take into account transaction fee when given a predicate", async () => {
144+
await expect(() =>
145+
sender.sendTransaction({
146+
to: receiver.address,
147+
gasPrice: 1,
148+
value: 200,
149+
})
150+
).to.changeEtherBalance(
151+
sender,
152+
(diff: bigint) => diff === BigInt(-(txGasFees + 200)),
153+
{
154+
includeFee: true,
155+
}
156+
);
157+
});
158+
131159
it("Should ignore fee if receiver's wallet is being checked and includeFee was set", async () => {
132160
await expect(() =>
133161
sender.sendTransaction({
@@ -148,6 +176,18 @@ describe("INTEGRATION: changeEtherBalance matcher", function () {
148176
).to.changeEtherBalance(sender, -200);
149177
});
150178

179+
it("Should pass on negative case when expected balance does not satisfy the predicate", async () => {
180+
await expect(() =>
181+
sender.sendTransaction({
182+
to: receiver.address,
183+
value: 200,
184+
})
185+
).to.not.changeEtherBalance(
186+
receiver,
187+
(diff: bigint) => diff === BigInt(300)
188+
);
189+
});
190+
151191
it("Should throw when fee was not calculated correctly", async () => {
152192
await expect(
153193
expect(() =>
@@ -179,6 +219,23 @@ describe("INTEGRATION: changeEtherBalance matcher", function () {
179219
);
180220
});
181221

222+
it("Should throw when actual balance change value does not satisfy the predicate", async () => {
223+
await expect(
224+
expect(() =>
225+
sender.sendTransaction({
226+
to: receiver.address,
227+
value: 200,
228+
})
229+
).to.changeEtherBalance(
230+
sender,
231+
(diff: bigint) => diff === BigInt(-500)
232+
)
233+
).to.be.eventually.rejectedWith(
234+
AssertionError,
235+
`Expected the ether balance change of "${sender.address}" to satisfy the predicate, but it didn't (balance change: -200 wei)`
236+
);
237+
});
238+
182239
it("Should throw in negative case when expected balance change value was equal to an actual", async () => {
183240
await expect(
184241
expect(() =>
@@ -193,6 +250,23 @@ describe("INTEGRATION: changeEtherBalance matcher", function () {
193250
);
194251
});
195252

253+
it("Should throw in negative case when expected balance change value satisfies the predicate", async () => {
254+
await expect(
255+
expect(() =>
256+
sender.sendTransaction({
257+
to: receiver.address,
258+
value: 200,
259+
})
260+
).to.not.changeEtherBalance(
261+
sender,
262+
(diff: bigint) => diff === BigInt(-200)
263+
)
264+
).to.be.eventually.rejectedWith(
265+
AssertionError,
266+
`Expected the ether balance change of "${sender.address}" to NOT satisfy the predicate, but it did (balance change: -200 wei)`
267+
);
268+
});
269+
196270
it("Should pass when given zero value tx", async () => {
197271
await expect(() =>
198272
sender.sendTransaction({ to: receiver.address, value: 0 })

0 commit comments

Comments
 (0)