-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2652 from NomicFoundation/francovictorio/hh-581/r…
…everted-with-panic Add revert matchers
- Loading branch information
Showing
22 changed files
with
2,359 additions
and
98 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { CustomError } from "hardhat/common"; | ||
|
||
export class HardhatChaiMatchersDecodingError extends CustomError { | ||
constructor(encodedData: string, type: string, parent: Error) { | ||
const message = `There was an error decoding '${encodedData}' as a ${type}`; | ||
|
||
super(message, parent); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,31 @@ | ||
import "./types"; | ||
import { supportBigNumber } from "./bigNumber"; | ||
import { supportEmit } from "./emit"; | ||
import { supportReverted } from "./reverted"; | ||
import { supportHexEqual } from "./hexEqual"; | ||
import { supportProperAddress } from "./properAddress"; | ||
import { supportProperPrivateKey } from "./properPrivateKey"; | ||
import { supportChangeEtherBalance } from "./changeEtherBalance"; | ||
import { supportChangeEtherBalances } from "./changeEtherBalances"; | ||
import { supportReverted } from "./reverted/reverted"; | ||
import { supportRevertedWith } from "./reverted/revertedWith"; | ||
import { supportRevertedWithCustomError } from "./reverted/revertedWithCustomError"; | ||
import { supportRevertedWithPanic } from "./reverted/revertedWithPanic"; | ||
import { supportRevertedWithoutReasonString } from "./reverted/revertedWithoutReasonString"; | ||
|
||
export function hardhatChaiMatchers( | ||
chai: Chai.ChaiStatic, | ||
utils: Chai.ChaiUtils | ||
) { | ||
supportBigNumber(chai.Assertion, utils); | ||
supportEmit(chai.Assertion); | ||
supportReverted(chai.Assertion); | ||
supportHexEqual(chai.Assertion); | ||
supportProperAddress(chai.Assertion); | ||
supportProperPrivateKey(chai.Assertion); | ||
supportChangeEtherBalance(chai.Assertion); | ||
supportChangeEtherBalances(chai.Assertion); | ||
supportReverted(chai.Assertion); | ||
supportRevertedWith(chai.Assertion); | ||
supportRevertedWithCustomError(chai.Assertion, utils); | ||
supportRevertedWithPanic(chai.Assertion); | ||
supportRevertedWithoutReasonString(chai.Assertion); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import type { BigNumber } from "ethers"; | ||
|
||
export const PANIC_CODES = { | ||
ASSERTION_ERROR: 0x1, | ||
ARITHMETIC_UNDER_OR_OVERFLOW: 0x11, | ||
DIVISION_BY_ZERO: 0x12, | ||
ENUM_CONVERSION_OUT_OF_BOUNDS: 0x21, | ||
INCORRECTLY_ENCODED_STORAGE_BYTE_ARRAY: 0x22, | ||
POP_ON_EMPTY_ARRAY: 0x31, | ||
ARRAY_ACCESS_OUT_OF_BOUNDS: 0x32, | ||
TOO_MUCH_MEMORY_ALLOCATED: 0x41, | ||
ZERO_INITIALIZED_VARIABLE: 0x51, | ||
}; | ||
|
||
// copied from hardhat-core | ||
export function panicErrorCodeToReason( | ||
errorCode: BigNumber | ||
): string | undefined { | ||
switch (errorCode.toNumber()) { | ||
case 0x1: | ||
return "Assertion error"; | ||
case 0x11: | ||
return "Arithmetic operation underflowed or overflowed outside of an unchecked block"; | ||
case 0x12: | ||
return "Division or modulo division by zero"; | ||
case 0x21: | ||
return "Tried to convert a value into an enum, but the value was too big or negative"; | ||
case 0x22: | ||
return "Incorrectly encoded storage byte array"; | ||
case 0x31: | ||
return ".pop() was called on an empty array"; | ||
case 0x32: | ||
return "Array accessed at an out-of-bounds or negative index"; | ||
case 0x41: | ||
return "Too much memory was allocated, or an array was created that is too large"; | ||
case 0x51: | ||
return "Called a zero-initialized variable of internal function type"; | ||
} | ||
} |
118 changes: 118 additions & 0 deletions
118
packages/hardhat-chai-matchers/src/reverted/reverted.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { decodeReturnData, getReturnDataFromError } from "./utils"; | ||
|
||
export function supportReverted(Assertion: Chai.AssertionStatic) { | ||
Assertion.addProperty("reverted", function (this: any) { | ||
const subject: unknown = this._obj; | ||
|
||
// Check if the received value can be linked to a transaction, and then | ||
// get the receipt of that transaction and check its status. | ||
// | ||
// If the value doesn't correspond to a transaction, then the `reverted` | ||
// assertion is false. | ||
const onSuccess = (value: unknown) => { | ||
if (isTransactionResponse(value) || typeof value === "string") { | ||
const hash = typeof value === "string" ? value : value.hash; | ||
|
||
if (!isValidTransactionHash(hash)) { | ||
throw new TypeError( | ||
`Expected a valid transaction hash, but got '${hash}'` | ||
); | ||
} | ||
|
||
return getTransactionReceipt(hash).then((receipt) => { | ||
this.assert( | ||
receipt.status === 0, | ||
"Expected transaction to be reverted", | ||
"Expected transaction NOT to be reverted" | ||
); | ||
}); | ||
} else if (isTransactionReceipt(value)) { | ||
const receipt = value; | ||
|
||
this.assert( | ||
receipt.status === 0, | ||
"Expected transaction to be reverted", | ||
"Expected transaction NOT to be reverted" | ||
); | ||
} else { | ||
// If the subject of the assertion is not connected to a transaction | ||
// (hash, receipt, etc.), then the assertion fails. | ||
// Since we use `false` here, this means that `.not.to.be.reverted` | ||
// assertions will pass instead of always throwing a validation error. | ||
// This allows users to do things like: | ||
// `expect(c.callStatic.f()).to.not.be.reverted` | ||
this.assert(false, "Expected transaction to be reverted"); | ||
} | ||
}; | ||
|
||
const onError = (error: any) => { | ||
const returnData = getReturnDataFromError(error); | ||
const decodedReturnData = decodeReturnData(returnData); | ||
|
||
if ( | ||
decodedReturnData.kind === "Empty" || | ||
decodedReturnData.kind === "Custom" | ||
) { | ||
// in the negated case, if we can't decode the reason, we just indicate | ||
// that the transaction didn't revert | ||
this.assert(true, null, `Expected transaction NOT to be reverted`); | ||
} else if (decodedReturnData.kind === "Error") { | ||
this.assert( | ||
true, | ||
null, | ||
`Expected transaction NOT to be reverted, but it reverted with reason '${decodedReturnData.reason}'` | ||
); | ||
} else if (decodedReturnData.kind === "Panic") { | ||
this.assert( | ||
true, | ||
null, | ||
`Expected transaction NOT to be reverted, but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ | ||
decodedReturnData.description | ||
})` | ||
); | ||
} else { | ||
const _exhaustiveCheck: never = decodedReturnData; | ||
} | ||
}; | ||
|
||
// we use `Promise.resolve(subject)` so we can process both values and | ||
// promises of values in the same way | ||
const derivedPromise = Promise.resolve(subject).then(onSuccess, onError); | ||
|
||
this.then = derivedPromise.then.bind(derivedPromise); | ||
this.catch = derivedPromise.catch.bind(derivedPromise); | ||
|
||
return this; | ||
}); | ||
} | ||
|
||
async function getTransactionReceipt(hash: string) { | ||
const hre = await import("hardhat"); | ||
|
||
return hre.ethers.provider.getTransactionReceipt(hash); | ||
} | ||
|
||
function isTransactionResponse(x: unknown): x is { hash: string } { | ||
if (typeof x === "object" && x !== null) { | ||
return "hash" in x; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function isTransactionReceipt(x: unknown): x is { status: number } { | ||
if (typeof x === "object" && x !== null && "status" in x) { | ||
const status = (x as any).status; | ||
|
||
// this means we only support ethers's receipts for now; adding support for | ||
// raw receipts, where the status is an hexadecimal string, should be easy | ||
// and we can do it if there's demand for that | ||
return typeof status === "number"; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
function isValidTransactionHash(x: string): boolean { | ||
return /0x[0-9a-fA-F]{64}/.test(x); | ||
} |
62 changes: 62 additions & 0 deletions
62
packages/hardhat-chai-matchers/src/reverted/revertedWith.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { decodeReturnData, getReturnDataFromError } from "./utils"; | ||
|
||
export function supportRevertedWith(Assertion: Chai.AssertionStatic) { | ||
Assertion.addMethod( | ||
"revertedWith", | ||
function (this: any, expectedReason: unknown) { | ||
// validate expected reason | ||
if (typeof expectedReason !== "string") { | ||
throw new TypeError("Expected a string as the expected reason string"); | ||
} | ||
|
||
const onSuccess = () => { | ||
this.assert( | ||
false, | ||
`Expected transaction to be reverted with reason '${expectedReason}', but it didn't revert` | ||
); | ||
}; | ||
|
||
const onError = (error: any) => { | ||
const returnData = getReturnDataFromError(error); | ||
const decodedReturnData = decodeReturnData(returnData); | ||
|
||
if (decodedReturnData.kind === "Empty") { | ||
this.assert( | ||
false, | ||
`Expected transaction to be reverted with reason '${expectedReason}', but it reverted without a reason string` | ||
); | ||
} else if (decodedReturnData.kind === "Error") { | ||
this.assert( | ||
decodedReturnData.reason === expectedReason, | ||
`Expected transaction to be reverted with reason '${expectedReason}', but it reverted with reason '${decodedReturnData.reason}'`, | ||
`Expected transaction NOT to be reverted with reason '${expectedReason}', but it was` | ||
); | ||
} else if (decodedReturnData.kind === "Panic") { | ||
this.assert( | ||
false, | ||
`Expected transaction to be reverted with reason '${expectedReason}', but it reverted with panic code ${decodedReturnData.code.toHexString()} (${ | ||
decodedReturnData.description | ||
})` | ||
); | ||
} else if (decodedReturnData.kind === "Custom") { | ||
this.assert( | ||
false, | ||
`Expected transaction to be reverted with reason '${expectedReason}', but it reverted with a custom error` | ||
); | ||
} else { | ||
const _exhaustiveCheck: never = decodedReturnData; | ||
} | ||
}; | ||
|
||
const derivedPromise = Promise.resolve(this._obj).then( | ||
onSuccess, | ||
onError | ||
); | ||
|
||
this.then = derivedPromise.then.bind(derivedPromise); | ||
this.catch = derivedPromise.catch.bind(derivedPromise); | ||
|
||
return this; | ||
} | ||
); | ||
} |
Oops, something went wrong.