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`",
|
"note": "More robust normalization of `uint256` types in `sign_typed_data_utils`",
|
||||||
"pr": 1742
|
"pr": 1742
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"note": "Add `RichRevertReason` type and associated utilities",
|
||||||
|
"pr": TODO
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"timestamp": 1563006338
|
"timestamp": 1563006338
|
||||||
|
@ -15,3 +15,8 @@ export { signTypedDataUtils } from './sign_typed_data_utils';
|
|||||||
export import AbiEncoder = require('./abi_encoder');
|
export import AbiEncoder = require('./abi_encoder');
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export { generatePseudoRandom256BitNumber } from './random';
|
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