UNPKG

@nomicfoundation/hardhat-chai-matchers

Version:
158 lines (132 loc) 5.14 kB
import type EthersT from "ethers"; import { buildAssert } from "../../utils"; import { REVERTED_MATCHER } from "../constants"; import { assertIsNotNull, preventAsyncMatcherChaining } from "../utils"; import { decodeReturnData, getReturnDataFromError, parseBytes32String, } from "./utils"; export function supportReverted( Assertion: Chai.AssertionStatic, chaiUtils: Chai.ChaiUtils ) { Assertion.addProperty(REVERTED_MATCHER, function (this: any) { // capture negated flag before async code executes; see buildAssert's jsdoc const negated = this.__flags.negate; const subject: unknown = this._obj; preventAsyncMatcherChaining(this, REVERTED_MATCHER, chaiUtils); // 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 = async (value: unknown) => { const assert = buildAssert(negated, onSuccess); 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}'` ); } const receipt = await getTransactionReceipt(hash); if (receipt === null) { // If the receipt is null, maybe the string is a bytes32 string if (isBytes32String(hash)) { assert(false, "Expected transaction to be reverted"); return; } } assertIsNotNull(receipt, "receipt"); assert( receipt.status === 0, "Expected transaction to be reverted", "Expected transaction NOT to be reverted" ); } else if (isTransactionReceipt(value)) { const receipt = value; 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` assert(false, "Expected transaction to be reverted"); } }; const onError = (error: any) => { const { toBeHex } = require("ethers") as typeof EthersT; const assert = buildAssert(negated, onError); 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 assert(true, undefined, `Expected transaction NOT to be reverted`); } else if (decodedReturnData.kind === "Error") { assert( true, undefined, `Expected transaction NOT to be reverted, but it reverted with reason '${decodedReturnData.reason}'` ); } else if (decodedReturnData.kind === "Panic") { assert( true, undefined, `Expected transaction NOT to be reverted, but it reverted with panic code ${toBeHex( decodedReturnData.code )} (${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); } function isBytes32String(v: string): boolean { try { parseBytes32String(v); return true; } catch { return false; } }