@0x/utils: Tweak RevertError to produce a meaningful Error message/stack trace.

`@0x/utils`: Add more `RevertError` decoding/transforming functions (such as `decodeThrownErrorAsRevertError()`)
This commit is contained in:
Lawrence Forman
2019-05-18 02:06:00 -04:00
committed by Amir Bandeali
parent d480f8d82a
commit eaa4373a18
4 changed files with 126 additions and 11 deletions

View File

@@ -17,6 +17,14 @@
{
"note": "Add `RevertError`, `StringRevertError`, `AnyRevertError` types and associated utilities",
"pr": 1761
},
{
"note": "Update `RevertError` construction to produce a readable `Error` message",
"pr": 1819
},
{
"note": "Add `Error` -> `RevertError` functions",
"pr": 1819
}
],
"timestamp": 1563006338

View File

@@ -16,7 +16,9 @@ export import AbiEncoder = require('./abi_encoder');
export * from './types';
export { generatePseudoRandom256BitNumber } from './random';
export {
decodeRevertError,
decodeBytesAsRevertError,
decodeThrownErrorAsRevertError,
coerceThrownErrorAsRevertError,
registerRevertErrorType,
RevertError,
StringRevertError,

View File

@@ -37,18 +37,57 @@ export function registerRevertErrorType(revertClass: RevertErrorType): void {
* @param bytes The ABI encoded revert error. Either a hex string or a Buffer.
* @return A RevertError object.
*/
export function decodeRevertError(bytes: string | Buffer): RevertError {
export function decodeBytesAsRevertError(bytes: string | Buffer): RevertError {
return RevertError.decode(bytes);
}
/**
* Decode a thrown error.
* Throws if the data cannot be decoded as a known RevertError type.
* @param error Any thrown error.
* @return A RevertError object.
*/
export function decodeThrownErrorAsRevertError(error: Error): RevertError {
if (error instanceof RevertError) {
return error;
}
return RevertError.decode(getThrownErrorRevertErrorBytes(error));
}
/**
* Coerce a thrown error into a `RevertError`. Always succeeds.
* @param error Any thrown error.
* @return A RevertError object.
*/
export function coerceThrownErrorAsRevertError(error: Error): RevertError {
if (error instanceof RevertError) {
return error;
}
try {
return decodeThrownErrorAsRevertError(error);
} catch (err) {
if (isGanacheTransactionRevertError(error)) {
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();
}
// Coerce plain errors into a StringRevertError.
return new StringRevertError(error.message);
}
}
/**
* Base type for revert errors.
*/
export abstract class RevertError extends Error {
// Map of types registered via `registerType`.
private static readonly _typeRegistry: ObjectMap<RevertErrorRegistryItem> = {};
public abi?: RevertErrorAbi;
public values: ValueMap = {};
public readonly abi?: RevertErrorAbi;
public readonly values: ValueMap = {};
/**
* Decode an ABI encoded revert error.
@@ -105,8 +144,8 @@ export abstract class RevertError extends Error {
* @param declaration Function-style declaration of the revert (e.g., Error(string message))
* @param values Optional mapping of parameters to values.
*/
protected constructor(declaration?: string, values?: ValueMap) {
super();
protected constructor(name: string, declaration?: string, values?: ValueMap) {
super(createErrorMessage(name, values));
if (declaration !== undefined) {
this.abi = declarationToAbi(declaration);
if (values !== undefined) {
@@ -115,7 +154,6 @@ export abstract class RevertError extends Error {
}
// Extending Error is tricky; we need to explicitly set the prototype.
Object.setPrototypeOf(this, new.target.prototype);
this.message = this.toString();
}
/**
@@ -248,12 +286,65 @@ export abstract class RevertError extends Error {
}
}
const GANACHE_TRANSACTION_REVERT_ERROR_MESSAGE = /^VM Exception while processing transaction: revert/;
const GETH_TRANSACTION_REVERT_ERROR_MESSAGE = /always failing transaction$/;
interface GanacheTransactionRevertResult {
error: 'revert';
program_counter: number;
return?: string;
reason?: string;
}
interface GanacheTransactionRevertError extends Error {
results: { [hash: string]: GanacheTransactionRevertResult };
hashes: string[];
}
/**
* Try to extract the ecnoded revert error bytes from a thrown `Error`.
*/
export function getThrownErrorRevertErrorBytes(error: Error | GanacheTransactionRevertError): string {
// 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).encode();
}
if (result.return !== undefined && result.return !== '0x') {
return result.return;
}
} else {
// Handle geth transaction reverts.
if (isGethTransactionRevertError(error)) {
// Geth transaction reverts are opaque, meaning no useful data is returned,
// so we do nothing.
}
}
throw new Error(`Cannot decode thrown Errror "${error.message}" as a RevertError`);
}
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);
}
/**
* RevertError type for standard string reverts.
*/
export class StringRevertError extends RevertError {
constructor(message?: string) {
super('Error(string message)', { message });
super('StringRevertError', 'Error(string message)', { message });
}
}
@@ -262,10 +353,24 @@ export class StringRevertError extends RevertError {
*/
export class AnyRevertError extends RevertError {
constructor() {
super();
super('AnyRevertError');
}
}
/**
* Create an error message for a RevertError.
* @param name The name of the RevertError.
* @param values The values for the RevertError.
*/
function createErrorMessage(name: string, values?: ValueMap): string {
if (values === undefined) {
return `${name}()`;
}
const _values = _.omitBy(values, (v: any) => _.isNil(v));
const inner = _.isEmpty(_values) ? '' : inspect(_values);
return `${name}(${inner})`;
}
/**
* Parse a solidity function declaration into a RevertErrorAbi object.
* @param declaration Function declaration (e.g., 'foo(uint256 bar)').

View File

@@ -17,7 +17,7 @@ class DescendantRevertError extends StringRevertError {
class CustomRevertError extends RevertError {
public constructor(message?: string) {
super('CustomRevertError(string message)', { message });
super('CustomRevertError', 'CustomRevertError(string message)', { message });
}
}
@@ -56,7 +56,7 @@ describe('RevertError', () => {
it('should throw when registering an already registered signature', () => {
class CustomRevertError2 extends RevertError {
public constructor() {
super(new CustomRevertError().signature, {});
super('CustomRevertError2', new CustomRevertError().signature, {});
}
}
expect(() => RevertError.registerType(CustomRevertError2)).to.throw();