Skip to content

Commit 974bec0

Browse files
authored
Merge pull request #5219 from NomicFoundation/predicate-functions-balance-matchers
Accept predicate functions in balance and token balance change matchers
2 parents 029680d + 9b98a50 commit 974bec0

10 files changed

+436
-112
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

+72-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,61 @@ export function supportChangeEtherBalances(
3737
chaiUtils
3838
);
3939

40+
validateInput(this._obj, accounts, balanceChanges);
41+
4042
const checkBalanceChanges = ([actualChanges, accountAddresses]: [
4143
bigint[],
4244
string[]
4345
]) => {
4446
const assert = buildAssert(negated, checkBalanceChanges);
4547

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

8797
const derivedPromise = Promise.all([
@@ -96,6 +106,28 @@ export function supportChangeEtherBalances(
96106
);
97107
}
98108

109+
function validateInput(
110+
obj: any,
111+
accounts: Array<Addressable | string>,
112+
balanceChanges: EthersT.BigNumberish[] | ((changes: bigint[]) => boolean)
113+
) {
114+
try {
115+
if (
116+
Array.isArray(balanceChanges) &&
117+
accounts.length !== balanceChanges.length
118+
) {
119+
throw new Error(
120+
`The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})`
121+
);
122+
}
123+
} catch (e) {
124+
// if the input validation fails, we discard the subject since it could
125+
// potentially be a rejected promise
126+
Promise.resolve(obj).catch(() => {});
127+
throw e;
128+
}
129+
}
130+
99131
export async function getBalanceChanges(
100132
transaction: TransactionResponse | Promise<TransactionResponse>,
101133
accounts: Array<Addressable | string>,

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 (token balance change: ${actualChange.toString()} wei)`,
73+
`Expected the balance of ${tokenDescription} tokens for "${address}" to NOT satisfy the predicate, but it did (token balance change: ${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: bigint[]) => 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: bigint[]) => boolean)
3535
): AsyncAssertion;
3636
}
3737

0 commit comments

Comments
 (0)