@nomicfoundation/hardhat-ethers-chai-matchers 3.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +52 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +16 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/internal/add-chai-matchers.d.ts +2 -0
- package/dist/src/internal/add-chai-matchers.d.ts.map +1 -0
- package/dist/src/internal/add-chai-matchers.js +41 -0
- package/dist/src/internal/add-chai-matchers.js.map +1 -0
- package/dist/src/internal/constants.d.ts +13 -0
- package/dist/src/internal/constants.d.ts.map +1 -0
- package/dist/src/internal/constants.js +13 -0
- package/dist/src/internal/constants.js.map +1 -0
- package/dist/src/internal/hook-handlers/network.d.ts +4 -0
- package/dist/src/internal/hook-handlers/network.d.ts.map +1 -0
- package/dist/src/internal/hook-handlers/network.js +15 -0
- package/dist/src/internal/hook-handlers/network.js.map +1 -0
- package/dist/src/internal/matchers/addressable.d.ts +2 -0
- package/dist/src/internal/matchers/addressable.d.ts.map +1 -0
- package/dist/src/internal/matchers/addressable.js +53 -0
- package/dist/src/internal/matchers/addressable.js.map +1 -0
- package/dist/src/internal/matchers/big-number.d.ts +2 -0
- package/dist/src/internal/matchers/big-number.d.ts.map +1 -0
- package/dist/src/internal/matchers/big-number.js +178 -0
- package/dist/src/internal/matchers/big-number.js.map +1 -0
- package/dist/src/internal/matchers/changeEtherBalance.d.ts +7 -0
- package/dist/src/internal/matchers/changeEtherBalance.d.ts.map +1 -0
- package/dist/src/internal/matchers/changeEtherBalance.js +77 -0
- package/dist/src/internal/matchers/changeEtherBalance.js.map +1 -0
- package/dist/src/internal/matchers/changeEtherBalances.d.ts +7 -0
- package/dist/src/internal/matchers/changeEtherBalances.d.ts.map +1 -0
- package/dist/src/internal/matchers/changeEtherBalances.js +96 -0
- package/dist/src/internal/matchers/changeEtherBalances.js.map +1 -0
- package/dist/src/internal/matchers/changeTokenBalance.d.ts +16 -0
- package/dist/src/internal/matchers/changeTokenBalance.d.ts.map +1 -0
- package/dist/src/internal/matchers/changeTokenBalance.js +148 -0
- package/dist/src/internal/matchers/changeTokenBalance.js.map +1 -0
- package/dist/src/internal/matchers/emit.d.ts +5 -0
- package/dist/src/internal/matchers/emit.d.ts.map +1 -0
- package/dist/src/internal/matchers/emit.js +122 -0
- package/dist/src/internal/matchers/emit.js.map +1 -0
- package/dist/src/internal/matchers/hexEqual.d.ts +2 -0
- package/dist/src/internal/matchers/hexEqual.d.ts.map +1 -0
- package/dist/src/internal/matchers/hexEqual.js +19 -0
- package/dist/src/internal/matchers/hexEqual.js.map +1 -0
- package/dist/src/internal/matchers/properAddress.d.ts +2 -0
- package/dist/src/internal/matchers/properAddress.d.ts.map +1 -0
- package/dist/src/internal/matchers/properAddress.js +7 -0
- package/dist/src/internal/matchers/properAddress.js.map +1 -0
- package/dist/src/internal/matchers/properHex.d.ts +2 -0
- package/dist/src/internal/matchers/properHex.d.ts.map +1 -0
- package/dist/src/internal/matchers/properHex.js +13 -0
- package/dist/src/internal/matchers/properHex.js.map +1 -0
- package/dist/src/internal/matchers/properPrivateKey.d.ts +2 -0
- package/dist/src/internal/matchers/properPrivateKey.d.ts.map +1 -0
- package/dist/src/internal/matchers/properPrivateKey.js +7 -0
- package/dist/src/internal/matchers/properPrivateKey.js.map +1 -0
- package/dist/src/internal/matchers/reverted/panic.d.ts +13 -0
- package/dist/src/internal/matchers/reverted/panic.d.ts.map +1 -0
- package/dist/src/internal/matchers/reverted/panic.js +36 -0
- package/dist/src/internal/matchers/reverted/panic.js.map +1 -0
- package/dist/src/internal/matchers/reverted/reverted.d.ts +2 -0
- package/dist/src/internal/matchers/reverted/reverted.d.ts.map +1 -0
- package/dist/src/internal/matchers/reverted/reverted.js +112 -0
- package/dist/src/internal/matchers/reverted/reverted.js.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWith.d.ts +2 -0
- package/dist/src/internal/matchers/reverted/revertedWith.d.ts.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWith.js +56 -0
- package/dist/src/internal/matchers/reverted/revertedWith.js.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWithCustomError.d.ts +5 -0
- package/dist/src/internal/matchers/reverted/revertedWithCustomError.d.ts.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWithCustomError.js +120 -0
- package/dist/src/internal/matchers/reverted/revertedWithCustomError.js.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWithPanic.d.ts +2 -0
- package/dist/src/internal/matchers/reverted/revertedWithPanic.d.ts.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWithPanic.js +74 -0
- package/dist/src/internal/matchers/reverted/revertedWithPanic.js.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWithoutReason.d.ts +2 -0
- package/dist/src/internal/matchers/reverted/revertedWithoutReason.d.ts.map +1 -0
- package/dist/src/internal/matchers/reverted/revertedWithoutReason.js +41 -0
- package/dist/src/internal/matchers/reverted/revertedWithoutReason.js.map +1 -0
- package/dist/src/internal/matchers/reverted/utils.d.ts +39 -0
- package/dist/src/internal/matchers/reverted/utils.d.ts.map +1 -0
- package/dist/src/internal/matchers/reverted/utils.js +108 -0
- package/dist/src/internal/matchers/reverted/utils.js.map +1 -0
- package/dist/src/internal/matchers/withArgs.d.ts +16 -0
- package/dist/src/internal/matchers/withArgs.d.ts.map +1 -0
- package/dist/src/internal/matchers/withArgs.js +88 -0
- package/dist/src/internal/matchers/withArgs.js.map +1 -0
- package/dist/src/internal/utils/account.d.ts +3 -0
- package/dist/src/internal/utils/account.d.ts.map +1 -0
- package/dist/src/internal/utils/account.js +15 -0
- package/dist/src/internal/utils/account.js.map +1 -0
- package/dist/src/internal/utils/asserts.d.ts +5 -0
- package/dist/src/internal/utils/asserts.d.ts.map +1 -0
- package/dist/src/internal/utils/asserts.js +73 -0
- package/dist/src/internal/utils/asserts.js.map +1 -0
- package/dist/src/internal/utils/balance.d.ts +8 -0
- package/dist/src/internal/utils/balance.d.ts.map +1 -0
- package/dist/src/internal/utils/balance.js +19 -0
- package/dist/src/internal/utils/balance.js.map +1 -0
- package/dist/src/internal/utils/bigint.d.ts +2 -0
- package/dist/src/internal/utils/bigint.d.ts.map +1 -0
- package/dist/src/internal/utils/bigint.js +4 -0
- package/dist/src/internal/utils/bigint.js.map +1 -0
- package/dist/src/internal/utils/build-assert.d.ts +19 -0
- package/dist/src/internal/utils/build-assert.d.ts.map +1 -0
- package/dist/src/internal/utils/build-assert.js +39 -0
- package/dist/src/internal/utils/build-assert.js.map +1 -0
- package/dist/src/internal/utils/ordinal.d.ts +8 -0
- package/dist/src/internal/utils/ordinal.d.ts.map +1 -0
- package/dist/src/internal/utils/ordinal.js +21 -0
- package/dist/src/internal/utils/ordinal.js.map +1 -0
- package/dist/src/internal/utils/prevent-chaining.d.ts +2 -0
- package/dist/src/internal/utils/prevent-chaining.d.ts.map +1 -0
- package/dist/src/internal/utils/prevent-chaining.js +17 -0
- package/dist/src/internal/utils/prevent-chaining.js.map +1 -0
- package/dist/src/internal/utils/ssfi.d.ts +4 -0
- package/dist/src/internal/utils/ssfi.d.ts.map +1 -0
- package/dist/src/internal/utils/ssfi.js +2 -0
- package/dist/src/internal/utils/ssfi.js.map +1 -0
- package/dist/src/internal/utils/typed.d.ts +2 -0
- package/dist/src/internal/utils/typed.d.ts.map +1 -0
- package/dist/src/internal/utils/typed.js +10 -0
- package/dist/src/internal/utils/typed.js.map +1 -0
- package/dist/src/panic.d.ts +2 -0
- package/dist/src/panic.d.ts.map +1 -0
- package/dist/src/panic.js +2 -0
- package/dist/src/panic.js.map +1 -0
- package/dist/src/type-extensions.d.ts +45 -0
- package/dist/src/type-extensions.d.ts.map +1 -0
- package/dist/src/type-extensions.js +2 -0
- package/dist/src/type-extensions.js.map +1 -0
- package/dist/src/withArgs.d.ts +2 -0
- package/dist/src/withArgs.d.ts.map +1 -0
- package/dist/src/withArgs.js +2 -0
- package/dist/src/withArgs.js.map +1 -0
- package/package.json +85 -0
- package/src/index.ts +21 -0
- package/src/internal/add-chai-matchers.ts +46 -0
- package/src/internal/constants.ts +13 -0
- package/src/internal/hook-handlers/network.ts +24 -0
- package/src/internal/matchers/addressable.ts +86 -0
- package/src/internal/matchers/big-number.ts +279 -0
- package/src/internal/matchers/changeEtherBalance.ts +138 -0
- package/src/internal/matchers/changeEtherBalances.ts +188 -0
- package/src/internal/matchers/changeTokenBalance.ts +295 -0
- package/src/internal/matchers/emit.ts +232 -0
- package/src/internal/matchers/hexEqual.ts +29 -0
- package/src/internal/matchers/properAddress.ts +12 -0
- package/src/internal/matchers/properHex.ts +29 -0
- package/src/internal/matchers/properPrivateKey.ts +12 -0
- package/src/internal/matchers/reverted/panic.ts +36 -0
- package/src/internal/matchers/reverted/reverted.ts +165 -0
- package/src/internal/matchers/reverted/revertedWith.ts +100 -0
- package/src/internal/matchers/reverted/revertedWithCustomError.ts +243 -0
- package/src/internal/matchers/reverted/revertedWithPanic.ts +118 -0
- package/src/internal/matchers/reverted/revertedWithoutReason.ts +73 -0
- package/src/internal/matchers/reverted/utils.ts +147 -0
- package/src/internal/matchers/withArgs.ts +139 -0
- package/src/internal/utils/account.ts +24 -0
- package/src/internal/utils/asserts.ts +156 -0
- package/src/internal/utils/balance.ts +39 -0
- package/src/internal/utils/bigint.ts +3 -0
- package/src/internal/utils/build-assert.ts +54 -0
- package/src/internal/utils/ordinal.ts +24 -0
- package/src/internal/utils/prevent-chaining.ts +33 -0
- package/src/internal/utils/ssfi.ts +6 -0
- package/src/internal/utils/typed.ts +9 -0
- package/src/panic.ts +1 -0
- package/src/type-extensions.ts +82 -0
- package/src/withArgs.ts +1 -0
@@ -0,0 +1,138 @@
|
|
1
|
+
import type { BalanceChangeOptions } from "../utils/balance.js";
|
2
|
+
import type { Addressable } from "ethers/address";
|
3
|
+
import type { TransactionResponse } from "ethers/providers";
|
4
|
+
import type { BigNumberish } from "ethers/utils";
|
5
|
+
import type { EthereumProvider } from "hardhat/types/providers";
|
6
|
+
|
7
|
+
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
|
8
|
+
import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex";
|
9
|
+
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
|
10
|
+
import { toBigInt } from "ethers/utils";
|
11
|
+
|
12
|
+
import { CHANGE_ETHER_BALANCE_MATCHER } from "../constants.js";
|
13
|
+
import { getAddressOf } from "../utils/account.js";
|
14
|
+
import {
|
15
|
+
assertCanBeConvertedToBigint,
|
16
|
+
assertIsNotNull,
|
17
|
+
} from "../utils/asserts.js";
|
18
|
+
import { buildAssert } from "../utils/build-assert.js";
|
19
|
+
import { preventAsyncMatcherChaining } from "../utils/prevent-chaining.js";
|
20
|
+
|
21
|
+
export function supportChangeEtherBalance(
|
22
|
+
Assertion: Chai.AssertionStatic,
|
23
|
+
chaiUtils: Chai.ChaiUtils,
|
24
|
+
): void {
|
25
|
+
Assertion.addMethod(
|
26
|
+
CHANGE_ETHER_BALANCE_MATCHER,
|
27
|
+
function (
|
28
|
+
this: any,
|
29
|
+
provider: EthereumProvider,
|
30
|
+
account: Addressable | string,
|
31
|
+
balanceChange: BigNumberish | ((change: bigint) => boolean),
|
32
|
+
options?: BalanceChangeOptions,
|
33
|
+
) {
|
34
|
+
// capture negated flag before async code executes; see buildAssert's jsdoc
|
35
|
+
const negated = this.__flags.negate;
|
36
|
+
const subject = this._obj;
|
37
|
+
|
38
|
+
preventAsyncMatcherChaining(
|
39
|
+
this,
|
40
|
+
CHANGE_ETHER_BALANCE_MATCHER,
|
41
|
+
chaiUtils,
|
42
|
+
);
|
43
|
+
|
44
|
+
const checkBalanceChange = ([actualChange, address]: [
|
45
|
+
bigint,
|
46
|
+
string,
|
47
|
+
]) => {
|
48
|
+
const assert = buildAssert(negated, checkBalanceChange);
|
49
|
+
|
50
|
+
if (typeof balanceChange === "function") {
|
51
|
+
assert(
|
52
|
+
balanceChange(actualChange),
|
53
|
+
`Expected the ether balance change of "${address}" to satisfy the predicate, but it didn't (balance change: ${actualChange.toString()} wei)`,
|
54
|
+
`Expected the ether balance change of "${address}" to NOT satisfy the predicate, but it did (balance change: ${actualChange.toString()} wei)`,
|
55
|
+
);
|
56
|
+
} else {
|
57
|
+
const expectedChange = toBigInt(balanceChange);
|
58
|
+
assert(
|
59
|
+
actualChange === expectedChange,
|
60
|
+
`Expected the ether balance of "${address}" to change by ${balanceChange.toString()} wei, but it changed by ${actualChange.toString()} wei`,
|
61
|
+
`Expected the ether balance of "${address}" NOT to change by ${balanceChange.toString()} wei, but it did`,
|
62
|
+
);
|
63
|
+
}
|
64
|
+
};
|
65
|
+
|
66
|
+
const derivedPromise = Promise.all([
|
67
|
+
getBalanceChange(provider, subject, account, options),
|
68
|
+
getAddressOf(account),
|
69
|
+
]).then(checkBalanceChange);
|
70
|
+
this.then = derivedPromise.then.bind(derivedPromise);
|
71
|
+
this.catch = derivedPromise.catch.bind(derivedPromise);
|
72
|
+
this.promise = derivedPromise;
|
73
|
+
return this;
|
74
|
+
},
|
75
|
+
);
|
76
|
+
}
|
77
|
+
|
78
|
+
export async function getBalanceChange(
|
79
|
+
provider: EthereumProvider,
|
80
|
+
transaction:
|
81
|
+
| TransactionResponse
|
82
|
+
| Promise<TransactionResponse>
|
83
|
+
| (() => Promise<TransactionResponse> | TransactionResponse),
|
84
|
+
account: Addressable | string,
|
85
|
+
options?: BalanceChangeOptions,
|
86
|
+
): Promise<bigint> {
|
87
|
+
let txResponse: TransactionResponse;
|
88
|
+
|
89
|
+
if (typeof transaction === "function") {
|
90
|
+
txResponse = await transaction();
|
91
|
+
} else {
|
92
|
+
txResponse = await transaction;
|
93
|
+
}
|
94
|
+
|
95
|
+
const txReceipt = await txResponse.wait();
|
96
|
+
assertIsNotNull(txReceipt, "txReceipt");
|
97
|
+
const txBlockNumber = txReceipt.blockNumber;
|
98
|
+
|
99
|
+
const block = await provider.request({
|
100
|
+
method: "eth_getBlockByHash",
|
101
|
+
params: [txReceipt.blockHash, false],
|
102
|
+
});
|
103
|
+
|
104
|
+
assertHardhatInvariant(
|
105
|
+
isObject(block) &&
|
106
|
+
Array.isArray(block.transactions) &&
|
107
|
+
block.transactions.length === 1,
|
108
|
+
"There should be only 1 transaction in the block",
|
109
|
+
);
|
110
|
+
|
111
|
+
const address = await getAddressOf(account);
|
112
|
+
|
113
|
+
const balanceAfterHex = await provider.request({
|
114
|
+
method: "eth_getBalance",
|
115
|
+
params: [address, numberToHexString(txBlockNumber)],
|
116
|
+
});
|
117
|
+
|
118
|
+
const balanceBeforeHex = await provider.request({
|
119
|
+
method: "eth_getBalance",
|
120
|
+
params: [address, numberToHexString(txBlockNumber - 1)],
|
121
|
+
});
|
122
|
+
|
123
|
+
assertCanBeConvertedToBigint(balanceAfterHex);
|
124
|
+
assertCanBeConvertedToBigint(balanceBeforeHex);
|
125
|
+
|
126
|
+
const balanceAfter = BigInt(balanceAfterHex);
|
127
|
+
const balanceBefore = BigInt(balanceBeforeHex);
|
128
|
+
|
129
|
+
if (options?.includeFee !== true && address === txResponse.from) {
|
130
|
+
const gasPrice = txReceipt.gasPrice;
|
131
|
+
const gasUsed = txReceipt.gasUsed;
|
132
|
+
const txFee = gasPrice * gasUsed;
|
133
|
+
|
134
|
+
return balanceAfter + txFee - balanceBefore;
|
135
|
+
} else {
|
136
|
+
return balanceAfter - balanceBefore;
|
137
|
+
}
|
138
|
+
}
|
@@ -0,0 +1,188 @@
|
|
1
|
+
import type { BalanceChangeOptions } from "../utils/balance.js";
|
2
|
+
import type { Addressable } from "ethers/address";
|
3
|
+
import type { TransactionResponse } from "ethers/providers";
|
4
|
+
import type { EthereumProvider } from "hardhat/types/providers";
|
5
|
+
|
6
|
+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
|
7
|
+
import { toBigInt } from "ethers/utils";
|
8
|
+
|
9
|
+
import { CHANGE_ETHER_BALANCES_MATCHER } from "../constants.js";
|
10
|
+
import { getAddressOf } from "../utils/account.js";
|
11
|
+
import { assertIsNotNull } from "../utils/asserts.js";
|
12
|
+
import { getAddresses, getBalances } from "../utils/balance.js";
|
13
|
+
import { buildAssert } from "../utils/build-assert.js";
|
14
|
+
import { ordinal } from "../utils/ordinal.js";
|
15
|
+
import { preventAsyncMatcherChaining } from "../utils/prevent-chaining.js";
|
16
|
+
|
17
|
+
export function supportChangeEtherBalances(
|
18
|
+
Assertion: Chai.AssertionStatic,
|
19
|
+
chaiUtils: Chai.ChaiUtils,
|
20
|
+
): void {
|
21
|
+
Assertion.addMethod(
|
22
|
+
CHANGE_ETHER_BALANCES_MATCHER,
|
23
|
+
function (
|
24
|
+
this: any,
|
25
|
+
provider: EthereumProvider,
|
26
|
+
accounts: Array<Addressable | string>,
|
27
|
+
balanceChanges: bigint[] | ((changes: bigint[]) => boolean),
|
28
|
+
options?: BalanceChangeOptions,
|
29
|
+
) {
|
30
|
+
// capture negated flag before async code executes; see buildAssert's jsdoc
|
31
|
+
const negated = this.__flags.negate;
|
32
|
+
|
33
|
+
let subject = this._obj;
|
34
|
+
if (typeof subject === "function") {
|
35
|
+
subject = subject();
|
36
|
+
}
|
37
|
+
|
38
|
+
preventAsyncMatcherChaining(
|
39
|
+
this,
|
40
|
+
CHANGE_ETHER_BALANCES_MATCHER,
|
41
|
+
chaiUtils,
|
42
|
+
);
|
43
|
+
|
44
|
+
validateInput(this._obj, accounts, balanceChanges);
|
45
|
+
|
46
|
+
const checkBalanceChanges = ([actualChanges, accountAddresses]: [
|
47
|
+
bigint[],
|
48
|
+
string[],
|
49
|
+
]) => {
|
50
|
+
const assert = buildAssert(negated, checkBalanceChanges);
|
51
|
+
|
52
|
+
if (typeof balanceChanges === "function") {
|
53
|
+
assert(
|
54
|
+
balanceChanges(actualChanges),
|
55
|
+
"Expected the balance changes of the accounts to satisfy the predicate, but they didn't",
|
56
|
+
"Expected the balance changes of the accounts to NOT satisfy the predicate, but they did",
|
57
|
+
);
|
58
|
+
} else {
|
59
|
+
assert(
|
60
|
+
actualChanges.every(
|
61
|
+
(change, ind) => change === toBigInt(balanceChanges[ind]),
|
62
|
+
),
|
63
|
+
() => {
|
64
|
+
const lines: string[] = [];
|
65
|
+
actualChanges.forEach((change: bigint, i) => {
|
66
|
+
if (change !== toBigInt(balanceChanges[i])) {
|
67
|
+
lines.push(
|
68
|
+
`Expected the ether balance of ${
|
69
|
+
accountAddresses[i]
|
70
|
+
} (the ${ordinal(
|
71
|
+
i + 1,
|
72
|
+
)} address in the list) to change by ${balanceChanges[
|
73
|
+
i
|
74
|
+
].toString()} wei, but it changed by ${change.toString()} wei`,
|
75
|
+
);
|
76
|
+
}
|
77
|
+
});
|
78
|
+
return lines.join("\n");
|
79
|
+
},
|
80
|
+
() => {
|
81
|
+
const lines: string[] = [];
|
82
|
+
actualChanges.forEach((change: bigint, i) => {
|
83
|
+
if (change === toBigInt(balanceChanges[i])) {
|
84
|
+
lines.push(
|
85
|
+
`Expected the ether balance of ${
|
86
|
+
accountAddresses[i]
|
87
|
+
} (the ${ordinal(
|
88
|
+
i + 1,
|
89
|
+
)} address in the list) NOT to change by ${balanceChanges[
|
90
|
+
i
|
91
|
+
].toString()} wei, but it did`,
|
92
|
+
);
|
93
|
+
}
|
94
|
+
});
|
95
|
+
return lines.join("\n");
|
96
|
+
},
|
97
|
+
);
|
98
|
+
}
|
99
|
+
};
|
100
|
+
|
101
|
+
const derivedPromise = Promise.all([
|
102
|
+
getBalanceChanges(provider, subject, accounts, options),
|
103
|
+
getAddresses(accounts),
|
104
|
+
]).then(checkBalanceChanges);
|
105
|
+
this.then = derivedPromise.then.bind(derivedPromise);
|
106
|
+
this.catch = derivedPromise.catch.bind(derivedPromise);
|
107
|
+
this.promise = derivedPromise;
|
108
|
+
return this;
|
109
|
+
},
|
110
|
+
);
|
111
|
+
}
|
112
|
+
|
113
|
+
function validateInput(
|
114
|
+
obj: any,
|
115
|
+
accounts: Array<Addressable | string>,
|
116
|
+
balanceChanges: bigint[] | ((changes: bigint[]) => boolean),
|
117
|
+
) {
|
118
|
+
try {
|
119
|
+
if (
|
120
|
+
Array.isArray(balanceChanges) &&
|
121
|
+
accounts.length !== balanceChanges.length
|
122
|
+
) {
|
123
|
+
throw new HardhatError(
|
124
|
+
HardhatError.ERRORS.CHAI_MATCHERS.ACCOUNTS_NUMBER_DIFFERENT_FROM_BALANCE_CHANGES,
|
125
|
+
{
|
126
|
+
accounts: accounts.length,
|
127
|
+
balanceChanges: balanceChanges.length,
|
128
|
+
},
|
129
|
+
);
|
130
|
+
}
|
131
|
+
} catch (e) {
|
132
|
+
// if the input validation fails, we discard the subject since it could
|
133
|
+
// potentially be a rejected promise
|
134
|
+
Promise.resolve(obj).catch(() => {});
|
135
|
+
throw e;
|
136
|
+
}
|
137
|
+
}
|
138
|
+
|
139
|
+
export async function getBalanceChanges(
|
140
|
+
provider: EthereumProvider,
|
141
|
+
transaction: TransactionResponse | Promise<TransactionResponse>,
|
142
|
+
accounts: Array<Addressable | string>,
|
143
|
+
options?: BalanceChangeOptions,
|
144
|
+
): Promise<bigint[]> {
|
145
|
+
const txResponse = await transaction;
|
146
|
+
|
147
|
+
const txReceipt = await txResponse.wait();
|
148
|
+
assertIsNotNull(txReceipt, "txReceipt");
|
149
|
+
const txBlockNumber = txReceipt.blockNumber;
|
150
|
+
|
151
|
+
const balancesAfter = await getBalances(provider, accounts, txBlockNumber);
|
152
|
+
const balancesBefore = await getBalances(
|
153
|
+
provider,
|
154
|
+
accounts,
|
155
|
+
txBlockNumber - 1,
|
156
|
+
);
|
157
|
+
|
158
|
+
const txFees = await getTxFees(accounts, txResponse, options);
|
159
|
+
|
160
|
+
return balancesAfter.map(
|
161
|
+
(balance, ind) => balance + txFees[ind] - balancesBefore[ind],
|
162
|
+
);
|
163
|
+
}
|
164
|
+
|
165
|
+
async function getTxFees(
|
166
|
+
accounts: Array<Addressable | string>,
|
167
|
+
txResponse: TransactionResponse,
|
168
|
+
options?: BalanceChangeOptions,
|
169
|
+
): Promise<bigint[]> {
|
170
|
+
return Promise.all(
|
171
|
+
accounts.map(async (account) => {
|
172
|
+
if (
|
173
|
+
options?.includeFee !== true &&
|
174
|
+
(await getAddressOf(account)) === txResponse.from
|
175
|
+
) {
|
176
|
+
const txReceipt = await txResponse.wait();
|
177
|
+
assertIsNotNull(txReceipt, "txReceipt");
|
178
|
+
const gasPrice = txReceipt.gasPrice ?? txResponse.gasPrice;
|
179
|
+
const gasUsed = txReceipt.gasUsed;
|
180
|
+
const txFee = gasPrice * gasUsed;
|
181
|
+
|
182
|
+
return txFee;
|
183
|
+
}
|
184
|
+
|
185
|
+
return 0n;
|
186
|
+
}),
|
187
|
+
);
|
188
|
+
}
|
@@ -0,0 +1,295 @@
|
|
1
|
+
import type {
|
2
|
+
Addressable,
|
3
|
+
BaseContract,
|
4
|
+
BaseContractMethod,
|
5
|
+
BigNumberish,
|
6
|
+
ContractTransactionResponse,
|
7
|
+
} from "ethers";
|
8
|
+
import type { TransactionResponse } from "ethers/providers";
|
9
|
+
import type { EthereumProvider } from "hardhat/types/providers";
|
10
|
+
|
11
|
+
import {
|
12
|
+
assertHardhatInvariant,
|
13
|
+
HardhatError,
|
14
|
+
} from "@nomicfoundation/hardhat-errors";
|
15
|
+
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
|
16
|
+
import { toBigInt } from "ethers/utils";
|
17
|
+
|
18
|
+
import {
|
19
|
+
CHANGE_TOKEN_BALANCES_MATCHER,
|
20
|
+
CHANGE_TOKEN_BALANCE_MATCHER,
|
21
|
+
} from "../constants.js";
|
22
|
+
import { getAddressOf } from "../utils/account.js";
|
23
|
+
import { assertIsNotNull } from "../utils/asserts.js";
|
24
|
+
import { buildAssert } from "../utils/build-assert.js";
|
25
|
+
import { preventAsyncMatcherChaining } from "../utils/prevent-chaining.js";
|
26
|
+
|
27
|
+
export type Token = BaseContract & {
|
28
|
+
balanceOf: BaseContractMethod<[string], bigint, bigint>;
|
29
|
+
name: BaseContractMethod<[], string, string>;
|
30
|
+
transfer: BaseContractMethod<
|
31
|
+
[string, BigNumberish],
|
32
|
+
boolean,
|
33
|
+
ContractTransactionResponse
|
34
|
+
>;
|
35
|
+
symbol: BaseContractMethod<[], string, string>;
|
36
|
+
};
|
37
|
+
|
38
|
+
export function supportChangeTokenBalance(
|
39
|
+
Assertion: Chai.AssertionStatic,
|
40
|
+
chaiUtils: Chai.ChaiUtils,
|
41
|
+
): void {
|
42
|
+
Assertion.addMethod(
|
43
|
+
CHANGE_TOKEN_BALANCE_MATCHER,
|
44
|
+
function (
|
45
|
+
this: any,
|
46
|
+
provider: EthereumProvider,
|
47
|
+
token: Token,
|
48
|
+
account: Addressable | string,
|
49
|
+
balanceChange: bigint | ((change: bigint) => boolean),
|
50
|
+
) {
|
51
|
+
// capture negated flag before async code executes; see buildAssert's jsdoc
|
52
|
+
const negated = this.__flags.negate;
|
53
|
+
|
54
|
+
let subject = this._obj;
|
55
|
+
if (typeof subject === "function") {
|
56
|
+
subject = subject();
|
57
|
+
}
|
58
|
+
|
59
|
+
preventAsyncMatcherChaining(
|
60
|
+
this,
|
61
|
+
CHANGE_TOKEN_BALANCE_MATCHER,
|
62
|
+
chaiUtils,
|
63
|
+
);
|
64
|
+
|
65
|
+
checkToken(token, CHANGE_TOKEN_BALANCE_MATCHER);
|
66
|
+
|
67
|
+
const checkBalanceChange = ([actualChange, address, tokenDescription]: [
|
68
|
+
bigint,
|
69
|
+
string,
|
70
|
+
string,
|
71
|
+
]) => {
|
72
|
+
const assert = buildAssert(negated, checkBalanceChange);
|
73
|
+
|
74
|
+
if (typeof balanceChange === "function") {
|
75
|
+
assert(
|
76
|
+
balanceChange(actualChange),
|
77
|
+
`Expected the balance of ${tokenDescription} tokens for "${address}" to satisfy the predicate, but it didn't (token balance change: ${actualChange.toString()} wei)`,
|
78
|
+
`Expected the balance of ${tokenDescription} tokens for "${address}" to NOT satisfy the predicate, but it did (token balance change: ${actualChange.toString()} wei)`,
|
79
|
+
);
|
80
|
+
} else {
|
81
|
+
assert(
|
82
|
+
actualChange === toBigInt(balanceChange),
|
83
|
+
`Expected the balance of ${tokenDescription} tokens for "${address}" to change by ${balanceChange.toString()}, but it changed by ${actualChange.toString()}`,
|
84
|
+
`Expected the balance of ${tokenDescription} tokens for "${address}" NOT to change by ${balanceChange.toString()}, but it did`,
|
85
|
+
);
|
86
|
+
}
|
87
|
+
};
|
88
|
+
|
89
|
+
const derivedPromise = Promise.all([
|
90
|
+
getBalanceChange(provider, subject, token, account),
|
91
|
+
getAddressOf(account),
|
92
|
+
getTokenDescription(token),
|
93
|
+
]).then(checkBalanceChange);
|
94
|
+
|
95
|
+
this.then = derivedPromise.then.bind(derivedPromise);
|
96
|
+
this.catch = derivedPromise.catch.bind(derivedPromise);
|
97
|
+
|
98
|
+
return this;
|
99
|
+
},
|
100
|
+
);
|
101
|
+
|
102
|
+
Assertion.addMethod(
|
103
|
+
CHANGE_TOKEN_BALANCES_MATCHER,
|
104
|
+
function (
|
105
|
+
this: any,
|
106
|
+
provider: EthereumProvider,
|
107
|
+
token: Token,
|
108
|
+
accounts: Array<Addressable | string>,
|
109
|
+
balanceChanges: bigint[] | ((changes: bigint[]) => boolean),
|
110
|
+
) {
|
111
|
+
// capture negated flag before async code executes; see buildAssert's jsdoc
|
112
|
+
const negated = this.__flags.negate;
|
113
|
+
|
114
|
+
let subject = this._obj;
|
115
|
+
if (typeof subject === "function") {
|
116
|
+
subject = subject();
|
117
|
+
}
|
118
|
+
|
119
|
+
preventAsyncMatcherChaining(
|
120
|
+
this,
|
121
|
+
CHANGE_TOKEN_BALANCES_MATCHER,
|
122
|
+
chaiUtils,
|
123
|
+
);
|
124
|
+
|
125
|
+
validateInput(this._obj, token, accounts, balanceChanges);
|
126
|
+
|
127
|
+
const balanceChangesPromise = Promise.all(
|
128
|
+
accounts.map((account) =>
|
129
|
+
getBalanceChange(provider, subject, token, account),
|
130
|
+
),
|
131
|
+
);
|
132
|
+
const addressesPromise = Promise.all(accounts.map(getAddressOf));
|
133
|
+
|
134
|
+
const checkBalanceChanges = ([
|
135
|
+
actualChanges,
|
136
|
+
addresses,
|
137
|
+
tokenDescription,
|
138
|
+
]: [bigint[], string[], string]) => {
|
139
|
+
const assert = buildAssert(negated, checkBalanceChanges);
|
140
|
+
|
141
|
+
if (typeof balanceChanges === "function") {
|
142
|
+
assert(
|
143
|
+
balanceChanges(actualChanges),
|
144
|
+
`Expected the balance changes of ${tokenDescription} to satisfy the predicate, but they didn't`,
|
145
|
+
`Expected the balance changes of ${tokenDescription} to NOT satisfy the predicate, but they did`,
|
146
|
+
);
|
147
|
+
} else {
|
148
|
+
assert(
|
149
|
+
actualChanges.every(
|
150
|
+
(change, ind) => change === toBigInt(balanceChanges[ind]),
|
151
|
+
),
|
152
|
+
`Expected the balances of ${tokenDescription} tokens for ${addresses.join(
|
153
|
+
", ",
|
154
|
+
)} to change by ${balanceChanges.join(
|
155
|
+
", ",
|
156
|
+
)}, respectively, but they changed by ${actualChanges.join(", ")}`,
|
157
|
+
`Expected the balances of ${tokenDescription} tokens for ${addresses.join(
|
158
|
+
", ",
|
159
|
+
)} NOT to change by ${balanceChanges.join(
|
160
|
+
", ",
|
161
|
+
)}, respectively, but they did`,
|
162
|
+
);
|
163
|
+
}
|
164
|
+
};
|
165
|
+
|
166
|
+
const derivedPromise = Promise.all([
|
167
|
+
balanceChangesPromise,
|
168
|
+
addressesPromise,
|
169
|
+
getTokenDescription(token),
|
170
|
+
]).then(checkBalanceChanges);
|
171
|
+
|
172
|
+
this.then = derivedPromise.then.bind(derivedPromise);
|
173
|
+
this.catch = derivedPromise.catch.bind(derivedPromise);
|
174
|
+
|
175
|
+
return this;
|
176
|
+
},
|
177
|
+
);
|
178
|
+
}
|
179
|
+
|
180
|
+
function validateInput(
|
181
|
+
obj: any,
|
182
|
+
token: Token,
|
183
|
+
accounts: Array<Addressable | string>,
|
184
|
+
balanceChanges: bigint[] | ((changes: bigint[]) => boolean),
|
185
|
+
) {
|
186
|
+
try {
|
187
|
+
checkToken(token, CHANGE_TOKEN_BALANCES_MATCHER);
|
188
|
+
|
189
|
+
if (
|
190
|
+
Array.isArray(balanceChanges) &&
|
191
|
+
accounts.length !== balanceChanges.length
|
192
|
+
) {
|
193
|
+
throw new HardhatError(
|
194
|
+
HardhatError.ERRORS.CHAI_MATCHERS.ACCOUNTS_NUMBER_DIFFERENT_FROM_BALANCE_CHANGES,
|
195
|
+
{
|
196
|
+
accounts: accounts.length,
|
197
|
+
balanceChanges: balanceChanges.length,
|
198
|
+
},
|
199
|
+
);
|
200
|
+
}
|
201
|
+
} catch (e) {
|
202
|
+
// if the input validation fails, we discard the subject since it could
|
203
|
+
// potentially be a rejected promise
|
204
|
+
Promise.resolve(obj).catch(() => {});
|
205
|
+
throw e;
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
function checkToken(token: unknown, method: string) {
|
210
|
+
if (!isObject(token) || token === null || !("interface" in token)) {
|
211
|
+
throw new HardhatError(
|
212
|
+
HardhatError.ERRORS.CHAI_MATCHERS.FIRST_ARGUMENT_MUST_BE_A_CONTRACT_INSTANCE,
|
213
|
+
{
|
214
|
+
method,
|
215
|
+
},
|
216
|
+
);
|
217
|
+
} else if (
|
218
|
+
isObject(token) &&
|
219
|
+
"interface" in token &&
|
220
|
+
isObject(token.interface) &&
|
221
|
+
"getFunction" in token.interface &&
|
222
|
+
typeof token.interface.getFunction === "function" &&
|
223
|
+
token.interface.getFunction("balanceOf") === null
|
224
|
+
) {
|
225
|
+
throw new HardhatError(
|
226
|
+
HardhatError.ERRORS.CHAI_MATCHERS.CONTRACT_IS_NOT_AN_ERC20_TOKEN,
|
227
|
+
);
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
export async function getBalanceChange(
|
232
|
+
provider: EthereumProvider,
|
233
|
+
transaction: TransactionResponse | Promise<TransactionResponse>,
|
234
|
+
token: Token,
|
235
|
+
account: Addressable | string,
|
236
|
+
): Promise<bigint> {
|
237
|
+
const txResponse = await transaction;
|
238
|
+
|
239
|
+
const txReceipt = await txResponse.wait();
|
240
|
+
assertIsNotNull(txReceipt, "txReceipt");
|
241
|
+
const txBlockNumber = txReceipt.blockNumber;
|
242
|
+
|
243
|
+
const block = await provider.request({
|
244
|
+
method: "eth_getBlockByHash",
|
245
|
+
params: [txReceipt.blockHash, false],
|
246
|
+
});
|
247
|
+
|
248
|
+
assertHardhatInvariant(
|
249
|
+
isObject(block) &&
|
250
|
+
Array.isArray(block.transactions) &&
|
251
|
+
block.transactions.length === 1,
|
252
|
+
"There should be only 1 transaction in the block",
|
253
|
+
);
|
254
|
+
|
255
|
+
const address = await getAddressOf(account);
|
256
|
+
|
257
|
+
const balanceAfter = await token.balanceOf(address, {
|
258
|
+
blockTag: txBlockNumber,
|
259
|
+
});
|
260
|
+
|
261
|
+
const balanceBefore = await token.balanceOf(address, {
|
262
|
+
blockTag: txBlockNumber - 1,
|
263
|
+
});
|
264
|
+
|
265
|
+
return toBigInt(balanceAfter) - balanceBefore;
|
266
|
+
}
|
267
|
+
|
268
|
+
let tokenDescriptionsCache: Record<string, string> = {};
|
269
|
+
/**
|
270
|
+
* Get a description for the given token. Use the symbol of the token if
|
271
|
+
* possible; if it doesn't exist, the name is used; if the name doesn't
|
272
|
+
* exist, the address of the token is used.
|
273
|
+
*/
|
274
|
+
async function getTokenDescription(token: Token): Promise<string> {
|
275
|
+
const tokenAddress = await token.getAddress();
|
276
|
+
if (tokenDescriptionsCache[tokenAddress] === undefined) {
|
277
|
+
let tokenDescription = `<token at ${tokenAddress}>`;
|
278
|
+
try {
|
279
|
+
tokenDescription = await token.symbol();
|
280
|
+
} catch (e) {
|
281
|
+
try {
|
282
|
+
tokenDescription = await token.name();
|
283
|
+
} catch (e2) {}
|
284
|
+
}
|
285
|
+
|
286
|
+
tokenDescriptionsCache[tokenAddress] = tokenDescription;
|
287
|
+
}
|
288
|
+
|
289
|
+
return tokenDescriptionsCache[tokenAddress];
|
290
|
+
}
|
291
|
+
|
292
|
+
// only used by tests
|
293
|
+
export function clearTokenDescriptionsCache(): void {
|
294
|
+
tokenDescriptionsCache = {};
|
295
|
+
}
|