Add RichRevertReason
type and utilities to @0x/utils
This commit is contained in:
parent
5955a541a3
commit
26643a489b
@ -13,6 +13,10 @@
|
||||
{
|
||||
"note": "More robust normalization of `uint256` types in `sign_typed_data_utils`",
|
||||
"pr": 1742
|
||||
},
|
||||
{
|
||||
"note": "Add `RichRevertReason` type and associated utilities",
|
||||
"pr": TODO
|
||||
}
|
||||
],
|
||||
"timestamp": 1563006338
|
||||
|
@ -15,3 +15,8 @@ export { signTypedDataUtils } from './sign_typed_data_utils';
|
||||
export import AbiEncoder = require('./abi_encoder');
|
||||
export * from './types';
|
||||
export { generatePseudoRandom256BitNumber } from './random';
|
||||
export {
|
||||
decodeRichRevertReason,
|
||||
registerRichRevertReason,
|
||||
RichRevertReason,
|
||||
} from './rich_reverts';
|
||||
|
282
packages/utils/src/rich_reverts.ts
Normal file
282
packages/utils/src/rich_reverts.ts
Normal file
@ -0,0 +1,282 @@
|
||||
import { ObjectMap } from '@0x/types';
|
||||
import { DataItem, RichRevertAbi } from 'ethereum-types';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import * as _ from 'lodash';
|
||||
import { inspect } from 'util';
|
||||
|
||||
import * as AbiEncoder from './abi_encoder';
|
||||
import { BigNumber } from './configured_bignumber';
|
||||
|
||||
type ArgTypes = string | BigNumber | number | boolean;
|
||||
type ValueMap = ObjectMap<ArgTypes | undefined>;
|
||||
type RichRevertReasonType = { new(): RichRevertReason };
|
||||
type RichRevertReasonDecoder = (hex: string) => ValueMap;
|
||||
|
||||
interface RichRevertReasonRegistryItem {
|
||||
type: RichRevertReasonType,
|
||||
decoder: RichRevertReasonDecoder
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a RichRevertReason type so that it can be decoded by
|
||||
* `decodeRevertReason`.
|
||||
* @param richRevertClass A class that inherits from RichRevertReason.
|
||||
*/
|
||||
export function registerRichRevertReason(richRevertClass: RichRevertReasonType) {
|
||||
RichRevertReason.registerType(richRevertClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an ABI encoded rich revert reason.
|
||||
* Throws if the data cannot be decoded as a known RichRevertReason type.
|
||||
* @param bytes The ABI encoded rich revert reason. Either a hex string or a Buffer.
|
||||
* @return A RichRevertReason object.
|
||||
*/
|
||||
export function decodeRichRevertReason(bytes: string | Buffer): RichRevertReason {
|
||||
return RichRevertReason.decode(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base type for rich revert reasons.
|
||||
*/
|
||||
export abstract class RichRevertReason {
|
||||
|
||||
// Map of types registered via `registerType`.
|
||||
static _typeRegistry: ObjectMap<RichRevertReasonRegistryItem> = {};
|
||||
|
||||
/**
|
||||
* Decode an ABI encoded rich revert reason.
|
||||
* Throws if the data cannot be decoded as a known RichRevertReason type.
|
||||
* @param bytes The ABI encoded rich revert reason. Either a hex string or a Buffer.
|
||||
* @return A RichRevertReason object.
|
||||
*/
|
||||
public static decode(bytes: string | Buffer): RichRevertReason {
|
||||
let _bytes: string;
|
||||
if (bytes instanceof Buffer) {
|
||||
_bytes = ethUtil.bufferToHex(bytes);
|
||||
} else {
|
||||
_bytes = bytes;
|
||||
}
|
||||
_bytes = ethUtil.addHexPrefix(_bytes);
|
||||
const selector = _bytes.slice(2, 10);
|
||||
const { decoder, type } = this._lookupType(selector);
|
||||
const instance = new type();
|
||||
try {
|
||||
const values = decoder(_bytes);
|
||||
return _.assign(instance, { values });
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Bytes ${shortenHex(_bytes)} cannot be decoded as rich revert ${instance.signature}: ${err.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a RichRevertReason type so that it can be decoded by
|
||||
* `decodeRevertReason`.
|
||||
* @param richRevertClass A class that inherits from RichRevertReason.
|
||||
*/
|
||||
public static registerType(richRevertClass: RichRevertReasonType) {
|
||||
const instance = new richRevertClass();
|
||||
RichRevertReason._typeRegistry[instance.selector] = {
|
||||
type: richRevertClass,
|
||||
decoder: createDecoder(instance.abi)
|
||||
};
|
||||
}
|
||||
|
||||
// Ge tthe registry info given a selector.
|
||||
private static _lookupType(selector: string): RichRevertReasonRegistryItem {
|
||||
if (selector in RichRevertReason._typeRegistry) {
|
||||
return RichRevertReason._typeRegistry[selector];
|
||||
}
|
||||
throw new Error(
|
||||
`Unknown rich revert reason selector "${selector}"`
|
||||
);
|
||||
}
|
||||
|
||||
public abi: RichRevertAbi;
|
||||
public values: ValueMap = {};
|
||||
|
||||
/**
|
||||
* Create a RichRevertReason instance with optional parameter values.
|
||||
* Parameters that are left undefined will not be tested in equality checks.
|
||||
* @param declaration Function-style declaration of the rich revert (e.g., StandardError(string message))
|
||||
* @param values Optional mapping of parameters to values.
|
||||
*/
|
||||
protected constructor(declaration: string, values?: ValueMap) {
|
||||
this.abi = declarationToAbi(declaration);
|
||||
if (values !== undefined) {
|
||||
_.assign(this.values, _.cloneDeep(values));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ABI name for this reason.
|
||||
*/
|
||||
get name(): string {
|
||||
return this.abi.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hex selector for this reason (without leading '0x').
|
||||
*/
|
||||
get selector(): string {
|
||||
return toSelector(this.abi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the signature for this reason: e.g., 'Error(string)'.
|
||||
*/
|
||||
get signature(): string {
|
||||
return toSignature(this.abi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ABI arguments for this reason.
|
||||
*/
|
||||
get arguments(): DataItem[] {
|
||||
return this.abi.arguments || [];
|
||||
}
|
||||
|
||||
private _getArgumentByName(name: string): DataItem {
|
||||
const arg = _.find(this.arguments, (a: DataItem) => a.name === name);
|
||||
if (_.isNil(arg)) {
|
||||
throw new Error(`RichRevertReason ${this.signature} has no argument named ${name}`);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this instance with another.
|
||||
* Fails if instances are not of the same type.
|
||||
* Only fields/values defined in both instances are compared.
|
||||
* @param other Either another RichRevertReason instance, hex-encoded bytes, or a Buffer of the ABI encoded reason.
|
||||
* @return True if both instances match.
|
||||
*/
|
||||
public equals(other: RichRevertReason | Buffer | string): boolean {
|
||||
let _other = other;
|
||||
if (_other instanceof Buffer) {
|
||||
_other = ethUtil.bufferToHex(_other);
|
||||
}
|
||||
if (typeof _other === 'string') {
|
||||
_other = RichRevertReason.decode(_other);
|
||||
}
|
||||
if (this.constructor !== _other.constructor) {
|
||||
return false;
|
||||
}
|
||||
for (const name of Object.keys(this.values)) {
|
||||
const a = this.values[name];
|
||||
const b = _other.values[name];
|
||||
if (a === b) {
|
||||
continue;
|
||||
}
|
||||
if (!_.isNil(a) && !_.isNil(b)) {
|
||||
const { type } = this._getArgumentByName(name);
|
||||
if (!checkArgEquality(type, a, b)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
const values = _.omitBy(this.values, (v: any) => _.isNil(v));
|
||||
const inner = _.isEmpty(values) ? '' : inspect(values);
|
||||
return `${this.constructor.name}(${inner})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a solidity function declaration into a RichRevertAbi object.
|
||||
* @param declaration Function declaration (e.g., 'foo(uint256 bar)').
|
||||
* @return A RichRevertAbi object.
|
||||
*/
|
||||
function declarationToAbi(declaration: string): RichRevertAbi {
|
||||
let m = /^\s*([_a-z][a-z0-9_]*)\((.*)\)\s*$/i.exec(declaration);
|
||||
if (!m) {
|
||||
throw new Error(`Invalid Revert Error signature: "${declaration}"`);
|
||||
}
|
||||
const [name, args] = m.slice(1);
|
||||
const argList: string[] = _.filter(args.split(','));
|
||||
const argData: DataItem[] = _.map(argList, (a: string) => {
|
||||
m = /^\s*([_a-z][a-z0-9_]*)\s+([_a-z][a-z0-9_]*)\s*$/i.exec(a);
|
||||
if (!m) {
|
||||
throw new Error(`Invalid Revert Error signature: "${declaration}"`);
|
||||
}
|
||||
return {
|
||||
name: m[2],
|
||||
type: m[1],
|
||||
};
|
||||
});
|
||||
const r: RichRevertAbi = {
|
||||
type: 'error',
|
||||
name,
|
||||
arguments: _.isEmpty(argData) ? [] : argData,
|
||||
};
|
||||
return r;
|
||||
}
|
||||
|
||||
function checkArgEquality(type: string, lhs: ArgTypes, rhs: ArgTypes): boolean {
|
||||
if (type === 'address') {
|
||||
return normalizeAddress(lhs as string) === normalizeAddress(rhs as string);
|
||||
} else if (type.startsWith('bytes')) {
|
||||
return normalizeBytes(lhs as string) === normalizeBytes(rhs as string);
|
||||
} else if (type === 'string') {
|
||||
return lhs === rhs;
|
||||
}
|
||||
// tslint:disable-next-line
|
||||
return new BigNumber((lhs as any) || 0).eq(rhs as any);
|
||||
}
|
||||
|
||||
function normalizeAddress(addr: string): string {
|
||||
const ADDRESS_SIZE = 20;
|
||||
return ethUtil.bufferToHex(
|
||||
ethUtil.setLengthLeft(
|
||||
ethUtil.toBuffer(ethUtil.addHexPrefix(addr)),
|
||||
ADDRESS_SIZE
|
||||
));
|
||||
}
|
||||
|
||||
function normalizeBytes(bytes: string): string {
|
||||
return ethUtil.addHexPrefix(bytes).toLowerCase();
|
||||
}
|
||||
|
||||
function shortenHex(hex: string, maxBytes: number = 4): string {
|
||||
const _hex = ethUtil.addHexPrefix(hex);
|
||||
if (_hex.length > maxBytes * 2 + 2) {
|
||||
const shortened = _hex.slice(0, maxBytes * 2 + 2);
|
||||
return `${shortened}...`;
|
||||
}
|
||||
return _hex;
|
||||
}
|
||||
|
||||
function createDecoder(abi: RichRevertAbi): (hex: string) => ValueMap {
|
||||
const encoder = AbiEncoder.createMethod(abi.name, abi.arguments || []);
|
||||
return (hex: string): ValueMap => {
|
||||
// tslint:disable-next-line
|
||||
return encoder.decode(hex) as ValueMap;
|
||||
};
|
||||
}
|
||||
|
||||
function toSignature(abi: RichRevertAbi): string {
|
||||
const argTypes = _.map(abi.arguments, (a: DataItem) => a.type);
|
||||
const args = argTypes.join(',');
|
||||
return `${abi.name}(${args})`;
|
||||
}
|
||||
|
||||
function toSelector(abi: RichRevertAbi): string {
|
||||
return ethUtil
|
||||
.sha3(Buffer.from(toSignature(abi)))
|
||||
.slice(0, 4)
|
||||
.toString('hex');
|
||||
}
|
||||
|
||||
export class StandardError extends RichRevertReason {
|
||||
constructor(message?: string) {
|
||||
super(
|
||||
'Error(string message)',
|
||||
{ message }
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user