@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,243 @@
1
+ import type { Ssfi } from "../../utils/ssfi.js";
2
+ import type { ErrorFragment, Interface } from "ethers/abi";
3
+ import type { BaseContract } from "ethers/contract";
4
+
5
+ import { HardhatError } from "@nomicfoundation/hardhat-errors";
6
+ import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex";
7
+
8
+ import {
9
+ ASSERTION_ABORTED,
10
+ REVERTED_WITH_CUSTOM_ERROR_MATCHER,
11
+ } from "../../constants.js";
12
+ import { assertArgsArraysEqual, assertIsNotNull } from "../../utils/asserts.js";
13
+ import { buildAssert } from "../../utils/build-assert.js";
14
+ import { preventAsyncMatcherChaining } from "../../utils/prevent-chaining.js";
15
+
16
+ import {
17
+ decodeReturnData,
18
+ getReturnDataFromError,
19
+ resultToArray,
20
+ } from "./utils.js";
21
+
22
+ export const REVERTED_WITH_CUSTOM_ERROR_CALLED = "customErrorAssertionCalled";
23
+
24
+ interface CustomErrorAssertionData {
25
+ contractInterface: Interface;
26
+ returnData: string;
27
+ customError: ErrorFragment;
28
+ }
29
+
30
+ export function supportRevertedWithCustomError(
31
+ Assertion: Chai.AssertionStatic,
32
+ chaiUtils: Chai.ChaiUtils,
33
+ ): void {
34
+ Assertion.addMethod(
35
+ REVERTED_WITH_CUSTOM_ERROR_MATCHER,
36
+ function (
37
+ this: any,
38
+ contract: BaseContract,
39
+ expectedCustomErrorName: string,
40
+ ...args: any[]
41
+ ) {
42
+ // capture negated flag before async code executes; see buildAssert's jsdoc
43
+ const negated = this.__flags.negate;
44
+
45
+ const { iface, expectedCustomError } = validateInput(
46
+ this._obj,
47
+ contract,
48
+ expectedCustomErrorName,
49
+ args,
50
+ );
51
+
52
+ preventAsyncMatcherChaining(
53
+ this,
54
+ REVERTED_WITH_CUSTOM_ERROR_MATCHER,
55
+ chaiUtils,
56
+ );
57
+
58
+ const onSuccess = () => {
59
+ if (chaiUtils.flag(this, ASSERTION_ABORTED) === true) {
60
+ return;
61
+ }
62
+
63
+ const assert = buildAssert(negated, onSuccess);
64
+
65
+ assert(
66
+ false,
67
+ `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it didn't revert`,
68
+ );
69
+ };
70
+
71
+ const onError = (error: any) => {
72
+ if (chaiUtils.flag(this, ASSERTION_ABORTED) === true) {
73
+ return;
74
+ }
75
+
76
+ const assert = buildAssert(negated, onError);
77
+
78
+ const returnData = getReturnDataFromError(error);
79
+ const decodedReturnData = decodeReturnData(returnData);
80
+
81
+ if (decodedReturnData.kind === "Empty") {
82
+ assert(
83
+ false,
84
+ `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted without a reason`,
85
+ );
86
+ } else if (decodedReturnData.kind === "Error") {
87
+ assert(
88
+ false,
89
+ `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted with reason '${decodedReturnData.reason}'`,
90
+ );
91
+ } else if (decodedReturnData.kind === "Panic") {
92
+ assert(
93
+ false,
94
+ `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted with panic code ${numberToHexString(
95
+ decodedReturnData.code,
96
+ )} (${decodedReturnData.description})`,
97
+ );
98
+ } else if (decodedReturnData.kind === "Custom") {
99
+ if (decodedReturnData.id === expectedCustomError.selector) {
100
+ // add flag with the data needed for .withArgs
101
+ const customErrorAssertionData: CustomErrorAssertionData = {
102
+ contractInterface: iface,
103
+ customError: expectedCustomError,
104
+ returnData,
105
+ };
106
+ this.customErrorData = customErrorAssertionData;
107
+
108
+ assert(
109
+ true,
110
+ undefined,
111
+ `Expected transaction NOT to be reverted with custom error '${expectedCustomErrorName}', but it was`,
112
+ );
113
+ } else {
114
+ // try to decode the actual custom error
115
+ // this will only work when the error comes from the given contract
116
+ const actualCustomError = iface.getError(decodedReturnData.id);
117
+
118
+ if (actualCustomError === null) {
119
+ assert(
120
+ false,
121
+ `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted with a different custom error`,
122
+ );
123
+ } else {
124
+ assert(
125
+ false,
126
+ `Expected transaction to be reverted with custom error '${expectedCustomErrorName}', but it reverted with custom error '${actualCustomError.name}'`,
127
+ );
128
+ }
129
+ }
130
+ } else {
131
+ const _exhaustiveCheck: never = decodedReturnData;
132
+ }
133
+ };
134
+
135
+ const derivedPromise = Promise.resolve(this._obj).then(
136
+ onSuccess,
137
+ onError,
138
+ );
139
+
140
+ // needed for .withArgs
141
+ chaiUtils.flag(this, REVERTED_WITH_CUSTOM_ERROR_CALLED, true);
142
+ this.promise = derivedPromise;
143
+
144
+ this.then = derivedPromise.then.bind(derivedPromise);
145
+ this.catch = derivedPromise.catch.bind(derivedPromise);
146
+
147
+ return this;
148
+ },
149
+ );
150
+ }
151
+
152
+ function validateInput(
153
+ obj: any,
154
+ contract: BaseContract,
155
+ expectedCustomErrorName: string,
156
+ args: any[],
157
+ ): { iface: Interface; expectedCustomError: ErrorFragment } {
158
+ try {
159
+ // check the case where users forget to pass the contract as the first
160
+ // argument
161
+ if (typeof contract === "string" || contract?.interface === undefined) {
162
+ // discard subject since it could potentially be a rejected promise
163
+ throw new HardhatError(
164
+ HardhatError.ERRORS.CHAI_MATCHERS.FIRST_ARGUMENT_MUST_BE_A_CONTRACT,
165
+ );
166
+ }
167
+
168
+ // validate custom error name
169
+ if (typeof expectedCustomErrorName !== "string") {
170
+ throw new HardhatError(
171
+ HardhatError.ERRORS.CHAI_MATCHERS.STRING_EXPECTED_AS_CUSTOM_ERROR_NAME,
172
+ );
173
+ }
174
+
175
+ const iface = contract.interface;
176
+ const expectedCustomError = iface.getError(expectedCustomErrorName);
177
+
178
+ // check that interface contains the given custom error
179
+ if (expectedCustomError === null) {
180
+ throw new HardhatError(
181
+ HardhatError.ERRORS.CHAI_MATCHERS.CONTRACT_DOES_NOT_HAVE_CUSTOM_ERROR,
182
+ {
183
+ customErrorName: expectedCustomErrorName,
184
+ },
185
+ );
186
+ }
187
+
188
+ if (args.length > 0) {
189
+ throw new HardhatError(
190
+ HardhatError.ERRORS.CHAI_MATCHERS.REVERT_INVALID_ARGUMENTS_LENGTH,
191
+ );
192
+ }
193
+
194
+ return { iface, expectedCustomError };
195
+ } catch (e) {
196
+ // if the input validation fails, we discard the subject since it could
197
+ // potentially be a rejected promise
198
+ Promise.resolve(obj).catch(() => {});
199
+ throw e;
200
+ }
201
+ }
202
+
203
+ export async function revertedWithCustomErrorWithArgs(
204
+ context: any,
205
+ Assertion: Chai.AssertionStatic,
206
+ _chaiUtils: Chai.ChaiUtils,
207
+ expectedArgs: any[],
208
+ ssfi: Ssfi,
209
+ ): Promise<void> {
210
+ const negated = false; // .withArgs cannot be negated
211
+ const assert = buildAssert(negated, ssfi);
212
+
213
+ const customErrorAssertionData: CustomErrorAssertionData =
214
+ context.customErrorData;
215
+
216
+ if (customErrorAssertionData === undefined) {
217
+ throw new HardhatError(
218
+ HardhatError.ERRORS.CHAI_MATCHERS.WITH_ARGS_FORBIDDEN,
219
+ );
220
+ }
221
+
222
+ const { contractInterface, customError, returnData } =
223
+ customErrorAssertionData;
224
+
225
+ const errorFragment = contractInterface.getError(customError.name);
226
+
227
+ assertIsNotNull(errorFragment, "errorFragment");
228
+
229
+ // We transform ether's Array-like object into an actual array as it's safer
230
+ const actualArgs = resultToArray(
231
+ contractInterface.decodeErrorResult(errorFragment, returnData),
232
+ );
233
+
234
+ assertArgsArraysEqual(
235
+ Assertion,
236
+ expectedArgs,
237
+ actualArgs,
238
+ `"${customError.name}" custom error`,
239
+ "error",
240
+ assert,
241
+ ssfi,
242
+ );
243
+ }
@@ -0,0 +1,118 @@
1
+ import { HardhatError } from "@nomicfoundation/hardhat-errors";
2
+ import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint";
3
+ import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex";
4
+
5
+ import { REVERTED_WITH_PANIC_MATCHER } from "../../constants.js";
6
+ import { buildAssert } from "../../utils/build-assert.js";
7
+ import { preventAsyncMatcherChaining } from "../../utils/prevent-chaining.js";
8
+
9
+ import { panicErrorCodeToReason } from "./panic.js";
10
+ import { decodeReturnData, getReturnDataFromError } from "./utils.js";
11
+
12
+ export function supportRevertedWithPanic(
13
+ Assertion: Chai.AssertionStatic,
14
+ chaiUtils: Chai.ChaiUtils,
15
+ ): void {
16
+ Assertion.addMethod(
17
+ REVERTED_WITH_PANIC_MATCHER,
18
+ function (this: any, expectedCodeArg: any) {
19
+ // capture negated flag before async code executes; see buildAssert's jsdoc
20
+ const negated = this.__flags.negate;
21
+
22
+ let expectedCode: bigint | undefined;
23
+ try {
24
+ if (expectedCodeArg !== undefined) {
25
+ expectedCode = toBigInt(expectedCodeArg);
26
+ }
27
+ } catch {
28
+ // if the input validation fails, we discard the subject since it could
29
+ // potentially be a rejected promise
30
+ Promise.resolve(this._obj).catch(() => {});
31
+
32
+ throw new HardhatError(
33
+ HardhatError.ERRORS.CHAI_MATCHERS.PANIC_CODE_EXPECTED,
34
+ {
35
+ panicCode: expectedCodeArg,
36
+ },
37
+ );
38
+ }
39
+
40
+ const code: bigint | undefined = expectedCode;
41
+
42
+ let description: string | undefined;
43
+ let formattedPanicCode: string;
44
+ if (code === undefined) {
45
+ formattedPanicCode = "some panic code";
46
+ } else {
47
+ const codeBN = toBigInt(code);
48
+ description = panicErrorCodeToReason(codeBN) ?? "unknown panic code";
49
+ formattedPanicCode = `panic code ${numberToHexString(codeBN)} (${description})`;
50
+ }
51
+
52
+ preventAsyncMatcherChaining(this, REVERTED_WITH_PANIC_MATCHER, chaiUtils);
53
+
54
+ const onSuccess = () => {
55
+ const assert = buildAssert(negated, onSuccess);
56
+
57
+ assert(
58
+ false,
59
+ `Expected transaction to be reverted with ${formattedPanicCode}, but it didn't revert`,
60
+ );
61
+ };
62
+
63
+ const onError = (error: any) => {
64
+ const assert = buildAssert(negated, onError);
65
+
66
+ const returnData = getReturnDataFromError(error);
67
+ const decodedReturnData = decodeReturnData(returnData);
68
+
69
+ if (decodedReturnData.kind === "Empty") {
70
+ assert(
71
+ false,
72
+ `Expected transaction to be reverted with ${formattedPanicCode}, but it reverted without a reason`,
73
+ );
74
+ } else if (decodedReturnData.kind === "Error") {
75
+ assert(
76
+ false,
77
+ `Expected transaction to be reverted with ${formattedPanicCode}, but it reverted with reason '${decodedReturnData.reason}'`,
78
+ );
79
+ } else if (decodedReturnData.kind === "Panic") {
80
+ if (code !== undefined) {
81
+ assert(
82
+ decodedReturnData.code === code,
83
+ `Expected transaction to be reverted with ${formattedPanicCode}, but it reverted with panic code ${numberToHexString(
84
+ decodedReturnData.code,
85
+ )} (${decodedReturnData.description})`,
86
+ `Expected transaction NOT to be reverted with ${formattedPanicCode}, but it was`,
87
+ );
88
+ } else {
89
+ assert(
90
+ true,
91
+ undefined,
92
+ `Expected transaction NOT to be reverted with ${formattedPanicCode}, but it reverted with panic code ${numberToHexString(
93
+ decodedReturnData.code,
94
+ )} (${decodedReturnData.description})`,
95
+ );
96
+ }
97
+ } else if (decodedReturnData.kind === "Custom") {
98
+ assert(
99
+ false,
100
+ `Expected transaction to be reverted with ${formattedPanicCode}, but it reverted with a custom error`,
101
+ );
102
+ } else {
103
+ const _exhaustiveCheck: never = decodedReturnData;
104
+ }
105
+ };
106
+
107
+ const derivedPromise = Promise.resolve(this._obj).then(
108
+ onSuccess,
109
+ onError,
110
+ );
111
+
112
+ this.then = derivedPromise.then.bind(derivedPromise);
113
+ this.catch = derivedPromise.catch.bind(derivedPromise);
114
+
115
+ return this;
116
+ },
117
+ );
118
+ }
@@ -0,0 +1,73 @@
1
+ import { numberToHexString } from "@nomicfoundation/hardhat-utils/hex";
2
+
3
+ import { REVERTED_WITHOUT_REASON_MATCHER } from "../../constants.js";
4
+ import { buildAssert } from "../../utils/build-assert.js";
5
+ import { preventAsyncMatcherChaining } from "../../utils/prevent-chaining.js";
6
+
7
+ import { decodeReturnData, getReturnDataFromError } from "./utils.js";
8
+
9
+ export function supportRevertedWithoutReason(
10
+ Assertion: Chai.AssertionStatic,
11
+ chaiUtils: Chai.ChaiUtils,
12
+ ): void {
13
+ Assertion.addMethod(REVERTED_WITHOUT_REASON_MATCHER, function (this: any) {
14
+ // capture negated flag before async code executes; see buildAssert's jsdoc
15
+ const negated = this.__flags.negate;
16
+
17
+ preventAsyncMatcherChaining(
18
+ this,
19
+ REVERTED_WITHOUT_REASON_MATCHER,
20
+ chaiUtils,
21
+ );
22
+
23
+ const onSuccess = () => {
24
+ const assert = buildAssert(negated, onSuccess);
25
+
26
+ assert(
27
+ false,
28
+ `Expected transaction to be reverted without a reason, but it didn't revert`,
29
+ );
30
+ };
31
+
32
+ const onError = (error: any) => {
33
+ const assert = buildAssert(negated, onError);
34
+
35
+ const returnData = getReturnDataFromError(error);
36
+ const decodedReturnData = decodeReturnData(returnData);
37
+
38
+ if (decodedReturnData.kind === "Error") {
39
+ assert(
40
+ false,
41
+ `Expected transaction to be reverted without a reason, but it reverted with reason '${decodedReturnData.reason}'`,
42
+ );
43
+ } else if (decodedReturnData.kind === "Empty") {
44
+ assert(
45
+ true,
46
+ undefined,
47
+ "Expected transaction NOT to be reverted without a reason, but it was",
48
+ );
49
+ } else if (decodedReturnData.kind === "Panic") {
50
+ assert(
51
+ false,
52
+ `Expected transaction to be reverted without a reason, but it reverted with panic code ${numberToHexString(
53
+ decodedReturnData.code,
54
+ )} (${decodedReturnData.description})`,
55
+ );
56
+ } else if (decodedReturnData.kind === "Custom") {
57
+ assert(
58
+ false,
59
+ `Expected transaction to be reverted without a reason, but it reverted with a custom error`,
60
+ );
61
+ } else {
62
+ const _exhaustiveCheck: never = decodedReturnData;
63
+ }
64
+ };
65
+
66
+ const derivedPromise = Promise.resolve(this._obj).then(onSuccess, onError);
67
+
68
+ this.then = derivedPromise.then.bind(derivedPromise);
69
+ this.catch = derivedPromise.catch.bind(derivedPromise);
70
+
71
+ return this;
72
+ });
73
+ }
@@ -0,0 +1,147 @@
1
+ import type { Result } from "ethers/abi";
2
+
3
+ import { HardhatError } from "@nomicfoundation/hardhat-errors";
4
+ import { ensureError } from "@nomicfoundation/hardhat-utils/error";
5
+ import { AssertionError } from "chai";
6
+ import { AbiCoder, decodeBytes32String } from "ethers/abi";
7
+
8
+ import { panicErrorCodeToReason } from "./panic.js";
9
+
10
+ // method id of 'Error(string)'
11
+ const ERROR_STRING_PREFIX = "0x08c379a0";
12
+
13
+ // method id of 'Panic(uint256)'
14
+ const PANIC_CODE_PREFIX = "0x4e487b71";
15
+
16
+ /**
17
+ * Try to obtain the return data of a transaction from the given value.
18
+ *
19
+ * If the value is an error but it doesn't have data, we assume it's not related
20
+ * to a reverted transaction and we re-throw it.
21
+ */
22
+ export function getReturnDataFromError(error: any): string {
23
+ if (!(error instanceof Error)) {
24
+ // eslint-disable-next-line no-restricted-syntax -- keep the original chai error structure
25
+ throw new AssertionError("Expected an Error object");
26
+ }
27
+
28
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- some properties do not exist in the default Error instance
29
+ const typedError = error as any;
30
+
31
+ const errorData = typedError.data ?? typedError.error?.data;
32
+
33
+ if (errorData === undefined) {
34
+ // eslint-disable-next-line no-restricted-syntax -- re-throw because the error is not related to a reverted transaction
35
+ throw error;
36
+ }
37
+
38
+ const returnData = typeof errorData === "string" ? errorData : errorData.data;
39
+
40
+ if (returnData === undefined || typeof returnData !== "string") {
41
+ // eslint-disable-next-line no-restricted-syntax -- re-throw because the error is not related to a reverted transaction
42
+ throw error;
43
+ }
44
+
45
+ return returnData;
46
+ }
47
+
48
+ type DecodedReturnData =
49
+ | {
50
+ kind: "Error";
51
+ reason: string;
52
+ }
53
+ | {
54
+ kind: "Empty";
55
+ }
56
+ | {
57
+ kind: "Panic";
58
+ code: bigint;
59
+ description: string;
60
+ }
61
+ | {
62
+ kind: "Custom";
63
+ id: string;
64
+ data: string;
65
+ };
66
+
67
+ export function decodeReturnData(returnData: string): DecodedReturnData {
68
+ const abi = new AbiCoder();
69
+
70
+ if (returnData === "0x") {
71
+ return { kind: "Empty" };
72
+ } else if (returnData.startsWith(ERROR_STRING_PREFIX)) {
73
+ const encodedReason = returnData.slice(ERROR_STRING_PREFIX.length);
74
+ let reason: string;
75
+
76
+ try {
77
+ reason = abi.decode(["string"], `0x${encodedReason}`)[0];
78
+ } catch (e) {
79
+ ensureError(e);
80
+
81
+ throw new HardhatError(HardhatError.ERRORS.CHAI_MATCHERS.DECODING_ERROR, {
82
+ encodedData: encodedReason,
83
+ type: "string",
84
+ reason: e.message,
85
+ });
86
+ }
87
+
88
+ return {
89
+ kind: "Error",
90
+ reason,
91
+ };
92
+ } else if (returnData.startsWith(PANIC_CODE_PREFIX)) {
93
+ const encodedReason = returnData.slice(PANIC_CODE_PREFIX.length);
94
+ let code: bigint;
95
+ try {
96
+ code = abi.decode(["uint256"], `0x${encodedReason}`)[0];
97
+ } catch (e) {
98
+ ensureError(e);
99
+
100
+ throw new HardhatError(HardhatError.ERRORS.CHAI_MATCHERS.DECODING_ERROR, {
101
+ encodedData: encodedReason,
102
+ type: "uint256",
103
+ reason: e.message,
104
+ });
105
+ }
106
+
107
+ const description = panicErrorCodeToReason(code) ?? "unknown panic code";
108
+
109
+ return {
110
+ kind: "Panic",
111
+ code,
112
+ description,
113
+ };
114
+ }
115
+
116
+ return {
117
+ kind: "Custom",
118
+ id: returnData.slice(0, 10),
119
+ data: `0x${returnData.slice(10)}`,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Takes an ethers result object and converts it into a (potentially nested) array.
125
+ *
126
+ * For example, given this error:
127
+ *
128
+ * struct Point(uint x, uint y)
129
+ * error MyError(string, Point)
130
+ *
131
+ * revert MyError("foo", Point(1, 2))
132
+ *
133
+ * The resulting array will be: ["foo", [1n, 2n]]
134
+ */
135
+ export function resultToArray(result: Result): any[] {
136
+ return result
137
+ .toArray()
138
+ .map((x) =>
139
+ typeof x === "object" && x !== null && "toArray" in x
140
+ ? resultToArray(x)
141
+ : x,
142
+ );
143
+ }
144
+
145
+ export function parseBytes32String(v: string): string {
146
+ return decodeBytes32String(v);
147
+ }