In @0x/dev-utils
: Break out RevertError
helper code into a separate file from chai_setup.ts
.
In `@0x/dev-utils`: Add chai support for ganache and geth transaction reverts.
This commit is contained in:
committed by
Amir Bandeali
parent
703a0fde3c
commit
1aae68c614
205
packages/dev-utils/src/chai_revert_error.ts
Normal file
205
packages/dev-utils/src/chai_revert_error.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { AnyRevertError, RevertError, StringRevertError } from '@0x/utils';
|
||||
|
||||
// tslint:disable no-namespace only-arrow-functions prefer-conditional-expression
|
||||
declare global {
|
||||
export namespace Chai {
|
||||
export interface Assertion {
|
||||
revertWith: (expected: string | RevertError) => Promise<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ChaiPromiseHandler = (x: any, ...rest: any[]) => Promise<void>;
|
||||
type ChaiAssertHandler = (x: any, ...rest: any[]) => void;
|
||||
|
||||
interface Chai {
|
||||
Assertion: ChaiAssertionType;
|
||||
}
|
||||
|
||||
interface ChaiAssertionInstance {
|
||||
assert: ChaiAssert;
|
||||
_obj: any;
|
||||
__flags: any;
|
||||
}
|
||||
|
||||
interface ChaiAssertionType {
|
||||
overwriteMethod: (name: string, _super: (expected: any) => any) => void;
|
||||
new (): ChaiAssertionInstance;
|
||||
}
|
||||
|
||||
type ChaiAssert = (
|
||||
condition: boolean,
|
||||
failMessage?: string,
|
||||
negatedFailMessage?: string,
|
||||
expected?: any,
|
||||
actual?: any,
|
||||
) => void;
|
||||
|
||||
interface GanacheTransactionRevertResult {
|
||||
error: 'revert';
|
||||
program_counter: number;
|
||||
return?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
interface GanacheTransactionRevertError extends Error {
|
||||
results: { [hash: string]: GanacheTransactionRevertResult };
|
||||
hashes: string[];
|
||||
}
|
||||
|
||||
export function revertErrorHelper(_chai: Chai): void {
|
||||
const proto = _chai.Assertion;
|
||||
proto.overwriteMethod('revertWith', function(_super: ChaiPromiseHandler): ChaiPromiseHandler {
|
||||
return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise<void> {
|
||||
const maybePromise = this._obj;
|
||||
// Make sure we're working with a promise.
|
||||
chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
|
||||
// Wait for the promise to reject.
|
||||
let resolveValue;
|
||||
let rejectValue: any;
|
||||
let didReject = false;
|
||||
try {
|
||||
resolveValue = await maybePromise;
|
||||
} catch (err) {
|
||||
rejectValue = err;
|
||||
didReject = true;
|
||||
}
|
||||
if (!didReject) {
|
||||
chaiFail(_chai, `Expected promise to reject but instead resolved with: ${resolveValue}`);
|
||||
}
|
||||
if (!compareRevertErrors.call(this, _chai, rejectValue, expected, true)) {
|
||||
// Wasn't handled by the comparison function so call the previous handler.
|
||||
_super.call(this, expected, ...rest);
|
||||
}
|
||||
};
|
||||
});
|
||||
proto.overwriteMethod('become', function(_super: ChaiPromiseHandler): ChaiPromiseHandler {
|
||||
return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise<void> {
|
||||
const maybePromise = this._obj;
|
||||
// Make sure we're working with a promise.
|
||||
chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
|
||||
// Wait for the promise to resolve.
|
||||
if (!compareRevertErrors.call(this, _chai, await maybePromise, expected)) {
|
||||
// Wasn't handled by the comparison function so call the previous handler.
|
||||
_super.call(this, expected, ...rest);
|
||||
}
|
||||
};
|
||||
});
|
||||
proto.overwriteMethod('equal', function(_super: ChaiAssertHandler): ChaiAssertHandler {
|
||||
return function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): void {
|
||||
if (!compareRevertErrors.call(this, _chai, this._obj, expected)) {
|
||||
// Wasn't handled by the comparison function so call the previous handler.
|
||||
_super.call(this, expected, ...rest);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two values as compatible RevertError types.
|
||||
* @return `true` if the comparison was fully evaluated. `false` indicates that
|
||||
* it should be deferred to another handler.
|
||||
*/
|
||||
function compareRevertErrors(
|
||||
this: ChaiAssertionInstance,
|
||||
_chai: Chai,
|
||||
_actual: any,
|
||||
_expected: any,
|
||||
force?: boolean,
|
||||
): boolean {
|
||||
let actual = _actual;
|
||||
let expected = _expected;
|
||||
// If either subject is a RevertError, or the `force` is `true`,
|
||||
// try to coerce the subjects into a RevertError.
|
||||
// Some of this is for convenience, some is for backwards-compatibility.
|
||||
if (force || expected instanceof RevertError || actual instanceof RevertError) {
|
||||
// `actual` can be a RevertError, string, or an Error type.
|
||||
if (!(actual instanceof RevertError)) {
|
||||
if (typeof actual === 'string') {
|
||||
actual = new StringRevertError(actual);
|
||||
} else if (actual instanceof Error) {
|
||||
actual = coerceErrorToRevertError(actual);
|
||||
} else {
|
||||
chaiAssert(_chai, false, `Result is not of type RevertError: ${actual}`);
|
||||
}
|
||||
}
|
||||
// `expected` can be a RevertError or string.
|
||||
if (typeof expected === 'string') {
|
||||
expected = new StringRevertError(expected);
|
||||
}
|
||||
}
|
||||
if (expected instanceof RevertError && actual instanceof RevertError) {
|
||||
// Check for equality.
|
||||
this.assert(
|
||||
actual.equals(expected),
|
||||
`${actual.toString()} != ${expected.toString()}`,
|
||||
`${actual.toString()} == ${expected.toString()}`,
|
||||
expected,
|
||||
actual,
|
||||
);
|
||||
// Return true to signal we handled it.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const GANACHE_TRANSACTION_REVERT_ERROR_MESSAGE = /^VM Exception while processing transaction: revert/;
|
||||
const GETH_TRANSACTION_REVERT_ERROR_MESSAGE = /always failing transaction$/;
|
||||
|
||||
function coerceErrorToRevertError(error: Error | GanacheTransactionRevertError): RevertError {
|
||||
// Handle ganache transaction reverts.
|
||||
if (isGanacheTransactionRevertError(error)) {
|
||||
// Grab the first result attached.
|
||||
const result = error.results[error.hashes[0]];
|
||||
// If a reason is provided, just wrap it in a StringRevertError
|
||||
if (result.reason !== undefined) {
|
||||
return new StringRevertError(result.reason);
|
||||
}
|
||||
// If there is return data, try to decode it.
|
||||
if (result.return !== undefined && result.return !== '0x') {
|
||||
return RevertError.decode(result.return);
|
||||
}
|
||||
// Otherwise, return an AnyRevertError type.
|
||||
return new AnyRevertError();
|
||||
}
|
||||
|
||||
// Handle geth transaction reverts.
|
||||
if (isGethTransactionRevertError(error)) {
|
||||
// Geth transaction reverts are opaque, meaning no useful data is returned,
|
||||
// so we just return an AnyRevertError type.
|
||||
return new AnyRevertError();
|
||||
}
|
||||
|
||||
// Handle call reverts.
|
||||
// `BaseContract` will throw a plain `Error` type for `StringRevertErrors`
|
||||
// in callAsync functions for backwards compatibility, and a proper RevertError
|
||||
// for all others.
|
||||
if (error instanceof RevertError) {
|
||||
return error;
|
||||
}
|
||||
// Coerce plain errors into a StringRevertError.
|
||||
return new StringRevertError(error.message);
|
||||
}
|
||||
|
||||
function isGanacheTransactionRevertError(
|
||||
error: Error | GanacheTransactionRevertError,
|
||||
): error is GanacheTransactionRevertError {
|
||||
if (GANACHE_TRANSACTION_REVERT_ERROR_MESSAGE.test(error.message) && 'hashes' in error && 'results' in error) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isGethTransactionRevertError(error: Error | GanacheTransactionRevertError): boolean {
|
||||
return GETH_TRANSACTION_REVERT_ERROR_MESSAGE.test(error.message);
|
||||
}
|
||||
|
||||
function chaiAssert(_chai: Chai, condition: boolean, failMessage?: string, expected?: any, actual?: any): void {
|
||||
const assert = new _chai.Assertion();
|
||||
assert.assert(condition, failMessage, undefined, expected, actual);
|
||||
}
|
||||
|
||||
function chaiFail(_chai: Chai, failMessage?: string, expected?: any, actual?: any): void {
|
||||
const assert = new _chai.Assertion();
|
||||
assert.assert(false, failMessage, undefined, expected, actual);
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
import { RevertError, StringRevertError } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import chaiAsPromised = require('chai-as-promised');
|
||||
import ChaiBigNumber = require('chai-bignumber');
|
||||
import * as dirtyChai from 'dirty-chai';
|
||||
|
||||
import { revertErrorHelper } from './chai_revert_error';
|
||||
|
||||
export const chaiSetup = {
|
||||
configure(): void {
|
||||
chai.config.includeStack = true;
|
||||
@@ -14,134 +15,3 @@ export const chaiSetup = {
|
||||
chai.use(dirtyChai);
|
||||
},
|
||||
};
|
||||
|
||||
// tslint:disable: only-arrow-functions prefer-conditional-expression
|
||||
|
||||
type ChaiPromiseHandler = (x: any, ...rest: any[]) => Promise<void>;
|
||||
type ChaiAssertHandler = (x: any, ...rest: any[]) => void;
|
||||
|
||||
interface Chai {
|
||||
Assertion: ChaiAssertionType;
|
||||
}
|
||||
|
||||
interface ChaiAssertionInstance {
|
||||
assert: ChaiAssert;
|
||||
_obj: any;
|
||||
__flags: any;
|
||||
}
|
||||
|
||||
interface ChaiAssertionType {
|
||||
overwriteMethod: (name: string, _super: (expected: any) => any) => void;
|
||||
new (): ChaiAssertionInstance;
|
||||
}
|
||||
|
||||
type ChaiAssert = (
|
||||
condition: boolean,
|
||||
failMessage?: string,
|
||||
negatedFailMessage?: string,
|
||||
expected?: any,
|
||||
actual?: any,
|
||||
) => void;
|
||||
|
||||
function revertErrorHelper(_chai: Chai): void {
|
||||
const proto = _chai.Assertion;
|
||||
proto.overwriteMethod('revertWith', function(_super: ChaiPromiseHandler): ChaiPromiseHandler {
|
||||
return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise<void> {
|
||||
const maybePromise = this._obj;
|
||||
// Make sure we're working with a promise.
|
||||
chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
|
||||
// Wait for the promise to reject.
|
||||
let resolveValue;
|
||||
let rejectValue: any;
|
||||
let didReject = false;
|
||||
try {
|
||||
resolveValue = await maybePromise;
|
||||
} catch (err) {
|
||||
rejectValue = err;
|
||||
didReject = true;
|
||||
}
|
||||
if (!didReject) {
|
||||
chaiFail(_chai, `Expected promise to reject but instead resolved with: ${resolveValue}`);
|
||||
}
|
||||
if (!compareRevertErrors.call(this, _chai, rejectValue, expected)) {
|
||||
// Wasn't handled by the comparison function so call the previous handler.
|
||||
_super.call(this, expected, ...rest);
|
||||
}
|
||||
};
|
||||
});
|
||||
proto.overwriteMethod('become', function(_super: ChaiPromiseHandler): ChaiPromiseHandler {
|
||||
return async function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): Promise<void> {
|
||||
const maybePromise = this._obj;
|
||||
// Make sure we're working with a promise.
|
||||
chaiAssert(_chai, maybePromise instanceof Promise, `Expected ${maybePromise} to be a promise`);
|
||||
// Wait for the promise to resolve.
|
||||
if (!compareRevertErrors.call(this, _chai, await maybePromise, expected)) {
|
||||
// Wasn't handled by the comparison function so call the previous handler.
|
||||
_super.call(this, expected, ...rest);
|
||||
}
|
||||
};
|
||||
});
|
||||
proto.overwriteMethod('equal', function(_super: ChaiAssertHandler): ChaiAssertHandler {
|
||||
return function(this: ChaiAssertionInstance, expected: any, ...rest: any[]): void {
|
||||
if (!compareRevertErrors.call(this, _chai, this._obj, expected)) {
|
||||
// Wasn't handled by the comparison function so call the previous handler.
|
||||
_super.call(this, expected, ...rest);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two values as compatible RevertError types.
|
||||
* @return `true` if the comparison was fully evaluated. `false` indicates that
|
||||
* it should be deferred to another handler.
|
||||
*/
|
||||
function compareRevertErrors(this: ChaiAssertionInstance, _chai: Chai, _actual: any, _expected: any): boolean {
|
||||
let actual = _actual;
|
||||
let expected = _expected;
|
||||
// If either subject is a RevertError, try to coerce the other into the same.
|
||||
// Some of this is for convenience, some is for backwards-compatibility.
|
||||
// TODO: Remove coercion of `actual` when all contracts and tests are upgraded
|
||||
// to explicitly use RevertErrors.
|
||||
if (expected instanceof RevertError || actual instanceof RevertError) {
|
||||
// `actual` can be a RevertError, string, or an Error type.
|
||||
if (!(actual instanceof RevertError)) {
|
||||
if (typeof actual === 'string') {
|
||||
actual = new StringRevertError(actual);
|
||||
} else if (actual instanceof Error) {
|
||||
// `BaseContract` will throw a plain `Error` type for `StringRevertErrors`
|
||||
// for backwards compatibility. So coerce it into a StringRevertError.
|
||||
actual = new StringRevertError(actual.message);
|
||||
} else {
|
||||
chaiAssert(_chai, false, `Result is not of type RevertError: ${actual}`);
|
||||
}
|
||||
}
|
||||
// `expected` can be a RevertError or string.
|
||||
if (typeof expected === 'string') {
|
||||
expected = new StringRevertError(expected);
|
||||
}
|
||||
}
|
||||
if (expected instanceof RevertError && actual instanceof RevertError) {
|
||||
// Check for equality.
|
||||
this.assert(
|
||||
actual.equals(expected),
|
||||
`${actual.toString()} != ${expected.toString()}`,
|
||||
`${actual.toString()} == ${expected.toString()}`,
|
||||
expected,
|
||||
actual,
|
||||
);
|
||||
// Return true to signal we handled it.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function chaiAssert(_chai: Chai, condition: boolean, failMessage?: string, expected?: any, actual?: any): void {
|
||||
const assert = new _chai.Assertion();
|
||||
assert.assert(condition, failMessage, undefined, expected, actual);
|
||||
}
|
||||
|
||||
function chaiFail(_chai: Chai, failMessage?: string, expected?: any, actual?: any): void {
|
||||
const assert = new _chai.Assertion();
|
||||
assert.assert(false, failMessage, undefined, expected, actual);
|
||||
}
|
||||
|
10
packages/dev-utils/src/globals.d.ts
vendored
10
packages/dev-utils/src/globals.d.ts
vendored
@@ -4,13 +4,3 @@ declare module '*.json' {
|
||||
export default json;
|
||||
/* tslint:enable */
|
||||
}
|
||||
|
||||
declare module 'chai' {
|
||||
global {
|
||||
export namespace Chai {
|
||||
export interface Assertion {
|
||||
revertWith: (expected: string | RevertError) => Promise<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -48,6 +48,58 @@ describe('Chai tests', () => {
|
||||
const error = new Error(message);
|
||||
expect(error).is.equal(revert);
|
||||
});
|
||||
it('should equate a ganache transaction revert error with reason to a StringRevertError with an equal message', () => {
|
||||
const message = 'foo';
|
||||
const error: any = new Error(`VM Exception while processing transaction: revert ${message}`);
|
||||
error.hashes = ['0x1'];
|
||||
error.results = { '0x1': { error: 'revert', program_counter: 1, return: '0x', reason: message } };
|
||||
const revert = new StringRevertError(message);
|
||||
expect(error).is.equal(revert);
|
||||
});
|
||||
it('should equate a ganache transaction revert error with return data to a StringRevertError with an equal message', () => {
|
||||
const error: any = new Error(`VM Exception while processing transaction: revert`);
|
||||
error.hashes = ['0x1'];
|
||||
// Encoding for `Error(string message='foo')`
|
||||
const returnData =
|
||||
'0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000';
|
||||
error.results = {
|
||||
'0x1': { error: 'revert', program_counter: 1, return: returnData, reason: undefined },
|
||||
};
|
||||
const revert = new StringRevertError('foo');
|
||||
expect(error).is.equal(revert);
|
||||
});
|
||||
it('should equate an empty ganache transaction revert error to any RevertError', () => {
|
||||
const error: any = new Error(`VM Exception while processing transaction: revert`);
|
||||
error.hashes = ['0x1'];
|
||||
error.results = { '0x1': { error: 'revert', program_counter: 1, return: '0x', reason: undefined } };
|
||||
const revert = new StringRevertError('foo');
|
||||
expect(error).is.equal(revert);
|
||||
});
|
||||
it('should not equate a ganache transaction revert error with reason to a StringRevertError with a different message', () => {
|
||||
const message = 'foo';
|
||||
const error: any = new Error(`VM Exception while processing transaction: revert ${message}`);
|
||||
error.hashes = ['0x1'];
|
||||
error.results = { '0x1': { error: 'revert', program_counter: 1, return: '0x', reason: message } };
|
||||
const revert = new StringRevertError('boo');
|
||||
expect(error).is.not.equal(revert);
|
||||
});
|
||||
it('should not equate a ganache transaction revert error with return data to a StringRevertError with a different message', () => {
|
||||
const error: any = new Error(`VM Exception while processing transaction: revert`);
|
||||
error.hashes = ['0x1'];
|
||||
// Encoding for `Error(string message='foo')`
|
||||
const returnData =
|
||||
'0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003666f6f0000000000000000000000000000000000000000000000000000000000';
|
||||
error.results = {
|
||||
'0x1': { error: 'revert', program_counter: 1, return: returnData, reason: undefined },
|
||||
};
|
||||
const revert = new StringRevertError('boo');
|
||||
expect(error).is.not.equal(revert);
|
||||
});
|
||||
it('should equate an opaque geth transaction revert error to any RevertError', () => {
|
||||
const error = new Error(`always failing transaction`);
|
||||
const revert = new StringRevertError('foo');
|
||||
expect(error).is.equal(revert);
|
||||
});
|
||||
it('should equate a string to a StringRevertError with the same message', () => {
|
||||
const message = 'foo';
|
||||
const revert = new StringRevertError(message);
|
||||
|
Reference in New Issue
Block a user