@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.
Files changed (173) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +52 -0
  3. package/dist/src/index.d.ts +5 -0
  4. package/dist/src/index.d.ts.map +1 -0
  5. package/dist/src/index.js +16 -0
  6. package/dist/src/index.js.map +1 -0
  7. package/dist/src/internal/add-chai-matchers.d.ts +2 -0
  8. package/dist/src/internal/add-chai-matchers.d.ts.map +1 -0
  9. package/dist/src/internal/add-chai-matchers.js +41 -0
  10. package/dist/src/internal/add-chai-matchers.js.map +1 -0
  11. package/dist/src/internal/constants.d.ts +13 -0
  12. package/dist/src/internal/constants.d.ts.map +1 -0
  13. package/dist/src/internal/constants.js +13 -0
  14. package/dist/src/internal/constants.js.map +1 -0
  15. package/dist/src/internal/hook-handlers/network.d.ts +4 -0
  16. package/dist/src/internal/hook-handlers/network.d.ts.map +1 -0
  17. package/dist/src/internal/hook-handlers/network.js +15 -0
  18. package/dist/src/internal/hook-handlers/network.js.map +1 -0
  19. package/dist/src/internal/matchers/addressable.d.ts +2 -0
  20. package/dist/src/internal/matchers/addressable.d.ts.map +1 -0
  21. package/dist/src/internal/matchers/addressable.js +53 -0
  22. package/dist/src/internal/matchers/addressable.js.map +1 -0
  23. package/dist/src/internal/matchers/big-number.d.ts +2 -0
  24. package/dist/src/internal/matchers/big-number.d.ts.map +1 -0
  25. package/dist/src/internal/matchers/big-number.js +178 -0
  26. package/dist/src/internal/matchers/big-number.js.map +1 -0
  27. package/dist/src/internal/matchers/changeEtherBalance.d.ts +7 -0
  28. package/dist/src/internal/matchers/changeEtherBalance.d.ts.map +1 -0
  29. package/dist/src/internal/matchers/changeEtherBalance.js +77 -0
  30. package/dist/src/internal/matchers/changeEtherBalance.js.map +1 -0
  31. package/dist/src/internal/matchers/changeEtherBalances.d.ts +7 -0
  32. package/dist/src/internal/matchers/changeEtherBalances.d.ts.map +1 -0
  33. package/dist/src/internal/matchers/changeEtherBalances.js +96 -0
  34. package/dist/src/internal/matchers/changeEtherBalances.js.map +1 -0
  35. package/dist/src/internal/matchers/changeTokenBalance.d.ts +16 -0
  36. package/dist/src/internal/matchers/changeTokenBalance.d.ts.map +1 -0
  37. package/dist/src/internal/matchers/changeTokenBalance.js +148 -0
  38. package/dist/src/internal/matchers/changeTokenBalance.js.map +1 -0
  39. package/dist/src/internal/matchers/emit.d.ts +5 -0
  40. package/dist/src/internal/matchers/emit.d.ts.map +1 -0
  41. package/dist/src/internal/matchers/emit.js +122 -0
  42. package/dist/src/internal/matchers/emit.js.map +1 -0
  43. package/dist/src/internal/matchers/hexEqual.d.ts +2 -0
  44. package/dist/src/internal/matchers/hexEqual.d.ts.map +1 -0
  45. package/dist/src/internal/matchers/hexEqual.js +19 -0
  46. package/dist/src/internal/matchers/hexEqual.js.map +1 -0
  47. package/dist/src/internal/matchers/properAddress.d.ts +2 -0
  48. package/dist/src/internal/matchers/properAddress.d.ts.map +1 -0
  49. package/dist/src/internal/matchers/properAddress.js +7 -0
  50. package/dist/src/internal/matchers/properAddress.js.map +1 -0
  51. package/dist/src/internal/matchers/properHex.d.ts +2 -0
  52. package/dist/src/internal/matchers/properHex.d.ts.map +1 -0
  53. package/dist/src/internal/matchers/properHex.js +13 -0
  54. package/dist/src/internal/matchers/properHex.js.map +1 -0
  55. package/dist/src/internal/matchers/properPrivateKey.d.ts +2 -0
  56. package/dist/src/internal/matchers/properPrivateKey.d.ts.map +1 -0
  57. package/dist/src/internal/matchers/properPrivateKey.js +7 -0
  58. package/dist/src/internal/matchers/properPrivateKey.js.map +1 -0
  59. package/dist/src/internal/matchers/reverted/panic.d.ts +13 -0
  60. package/dist/src/internal/matchers/reverted/panic.d.ts.map +1 -0
  61. package/dist/src/internal/matchers/reverted/panic.js +36 -0
  62. package/dist/src/internal/matchers/reverted/panic.js.map +1 -0
  63. package/dist/src/internal/matchers/reverted/reverted.d.ts +2 -0
  64. package/dist/src/internal/matchers/reverted/reverted.d.ts.map +1 -0
  65. package/dist/src/internal/matchers/reverted/reverted.js +112 -0
  66. package/dist/src/internal/matchers/reverted/reverted.js.map +1 -0
  67. package/dist/src/internal/matchers/reverted/revertedWith.d.ts +2 -0
  68. package/dist/src/internal/matchers/reverted/revertedWith.d.ts.map +1 -0
  69. package/dist/src/internal/matchers/reverted/revertedWith.js +56 -0
  70. package/dist/src/internal/matchers/reverted/revertedWith.js.map +1 -0
  71. package/dist/src/internal/matchers/reverted/revertedWithCustomError.d.ts +5 -0
  72. package/dist/src/internal/matchers/reverted/revertedWithCustomError.d.ts.map +1 -0
  73. package/dist/src/internal/matchers/reverted/revertedWithCustomError.js +120 -0
  74. package/dist/src/internal/matchers/reverted/revertedWithCustomError.js.map +1 -0
  75. package/dist/src/internal/matchers/reverted/revertedWithPanic.d.ts +2 -0
  76. package/dist/src/internal/matchers/reverted/revertedWithPanic.d.ts.map +1 -0
  77. package/dist/src/internal/matchers/reverted/revertedWithPanic.js +74 -0
  78. package/dist/src/internal/matchers/reverted/revertedWithPanic.js.map +1 -0
  79. package/dist/src/internal/matchers/reverted/revertedWithoutReason.d.ts +2 -0
  80. package/dist/src/internal/matchers/reverted/revertedWithoutReason.d.ts.map +1 -0
  81. package/dist/src/internal/matchers/reverted/revertedWithoutReason.js +41 -0
  82. package/dist/src/internal/matchers/reverted/revertedWithoutReason.js.map +1 -0
  83. package/dist/src/internal/matchers/reverted/utils.d.ts +39 -0
  84. package/dist/src/internal/matchers/reverted/utils.d.ts.map +1 -0
  85. package/dist/src/internal/matchers/reverted/utils.js +108 -0
  86. package/dist/src/internal/matchers/reverted/utils.js.map +1 -0
  87. package/dist/src/internal/matchers/withArgs.d.ts +16 -0
  88. package/dist/src/internal/matchers/withArgs.d.ts.map +1 -0
  89. package/dist/src/internal/matchers/withArgs.js +88 -0
  90. package/dist/src/internal/matchers/withArgs.js.map +1 -0
  91. package/dist/src/internal/utils/account.d.ts +3 -0
  92. package/dist/src/internal/utils/account.d.ts.map +1 -0
  93. package/dist/src/internal/utils/account.js +15 -0
  94. package/dist/src/internal/utils/account.js.map +1 -0
  95. package/dist/src/internal/utils/asserts.d.ts +5 -0
  96. package/dist/src/internal/utils/asserts.d.ts.map +1 -0
  97. package/dist/src/internal/utils/asserts.js +73 -0
  98. package/dist/src/internal/utils/asserts.js.map +1 -0
  99. package/dist/src/internal/utils/balance.d.ts +8 -0
  100. package/dist/src/internal/utils/balance.d.ts.map +1 -0
  101. package/dist/src/internal/utils/balance.js +19 -0
  102. package/dist/src/internal/utils/balance.js.map +1 -0
  103. package/dist/src/internal/utils/bigint.d.ts +2 -0
  104. package/dist/src/internal/utils/bigint.d.ts.map +1 -0
  105. package/dist/src/internal/utils/bigint.js +4 -0
  106. package/dist/src/internal/utils/bigint.js.map +1 -0
  107. package/dist/src/internal/utils/build-assert.d.ts +19 -0
  108. package/dist/src/internal/utils/build-assert.d.ts.map +1 -0
  109. package/dist/src/internal/utils/build-assert.js +39 -0
  110. package/dist/src/internal/utils/build-assert.js.map +1 -0
  111. package/dist/src/internal/utils/ordinal.d.ts +8 -0
  112. package/dist/src/internal/utils/ordinal.d.ts.map +1 -0
  113. package/dist/src/internal/utils/ordinal.js +21 -0
  114. package/dist/src/internal/utils/ordinal.js.map +1 -0
  115. package/dist/src/internal/utils/prevent-chaining.d.ts +2 -0
  116. package/dist/src/internal/utils/prevent-chaining.d.ts.map +1 -0
  117. package/dist/src/internal/utils/prevent-chaining.js +17 -0
  118. package/dist/src/internal/utils/prevent-chaining.js.map +1 -0
  119. package/dist/src/internal/utils/ssfi.d.ts +4 -0
  120. package/dist/src/internal/utils/ssfi.d.ts.map +1 -0
  121. package/dist/src/internal/utils/ssfi.js +2 -0
  122. package/dist/src/internal/utils/ssfi.js.map +1 -0
  123. package/dist/src/internal/utils/typed.d.ts +2 -0
  124. package/dist/src/internal/utils/typed.d.ts.map +1 -0
  125. package/dist/src/internal/utils/typed.js +10 -0
  126. package/dist/src/internal/utils/typed.js.map +1 -0
  127. package/dist/src/panic.d.ts +2 -0
  128. package/dist/src/panic.d.ts.map +1 -0
  129. package/dist/src/panic.js +2 -0
  130. package/dist/src/panic.js.map +1 -0
  131. package/dist/src/type-extensions.d.ts +45 -0
  132. package/dist/src/type-extensions.d.ts.map +1 -0
  133. package/dist/src/type-extensions.js +2 -0
  134. package/dist/src/type-extensions.js.map +1 -0
  135. package/dist/src/withArgs.d.ts +2 -0
  136. package/dist/src/withArgs.d.ts.map +1 -0
  137. package/dist/src/withArgs.js +2 -0
  138. package/dist/src/withArgs.js.map +1 -0
  139. package/package.json +85 -0
  140. package/src/index.ts +21 -0
  141. package/src/internal/add-chai-matchers.ts +46 -0
  142. package/src/internal/constants.ts +13 -0
  143. package/src/internal/hook-handlers/network.ts +24 -0
  144. package/src/internal/matchers/addressable.ts +86 -0
  145. package/src/internal/matchers/big-number.ts +279 -0
  146. package/src/internal/matchers/changeEtherBalance.ts +138 -0
  147. package/src/internal/matchers/changeEtherBalances.ts +188 -0
  148. package/src/internal/matchers/changeTokenBalance.ts +295 -0
  149. package/src/internal/matchers/emit.ts +232 -0
  150. package/src/internal/matchers/hexEqual.ts +29 -0
  151. package/src/internal/matchers/properAddress.ts +12 -0
  152. package/src/internal/matchers/properHex.ts +29 -0
  153. package/src/internal/matchers/properPrivateKey.ts +12 -0
  154. package/src/internal/matchers/reverted/panic.ts +36 -0
  155. package/src/internal/matchers/reverted/reverted.ts +165 -0
  156. package/src/internal/matchers/reverted/revertedWith.ts +100 -0
  157. package/src/internal/matchers/reverted/revertedWithCustomError.ts +243 -0
  158. package/src/internal/matchers/reverted/revertedWithPanic.ts +118 -0
  159. package/src/internal/matchers/reverted/revertedWithoutReason.ts +73 -0
  160. package/src/internal/matchers/reverted/utils.ts +147 -0
  161. package/src/internal/matchers/withArgs.ts +139 -0
  162. package/src/internal/utils/account.ts +24 -0
  163. package/src/internal/utils/asserts.ts +156 -0
  164. package/src/internal/utils/balance.ts +39 -0
  165. package/src/internal/utils/bigint.ts +3 -0
  166. package/src/internal/utils/build-assert.ts +54 -0
  167. package/src/internal/utils/ordinal.ts +24 -0
  168. package/src/internal/utils/prevent-chaining.ts +33 -0
  169. package/src/internal/utils/ssfi.ts +6 -0
  170. package/src/internal/utils/typed.ts +9 -0
  171. package/src/panic.ts +1 -0
  172. package/src/type-extensions.ts +82 -0
  173. 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
+ }