UNPKG

vue-blocklink

Version:

Vue support for the Blockchain Link browser extension

592 lines (543 loc) 20.1 kB
import {ObjectMap} from '../0xtypes'; import {DataItem, RevertErrorAbi} from '../types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import {inspect} from 'util'; import * as AbiEncoder from './abi_encoder'; import {BigNumber,B} from './configured_bignumber'; // tslint:disable: max-classes-per-file no-unnecessary-type-assertion type ArgTypes = | string | BigNumber | number | boolean | RevertError | BigNumber[] | string[] | number[] | boolean[] | Array<BigNumber | number | string>; type ValueMap = ObjectMap<ArgTypes | undefined>; type RevertErrorDecoder = (hex: string) => ValueMap; interface RevertErrorType { new(): RevertError; } interface RevertErrorRegistryItem { type: RevertErrorType; decoder: RevertErrorDecoder; } /** * Register a RevertError type so that it can be decoded by * `decodeRevertError`. * @param revertClass A class that inherits from RevertError. * @param force Allow overwriting registered types. */ export function registerRevertErrorType(revertClass: RevertErrorType, force: boolean = false): void { RevertError.registerType(revertClass, force); } /** * Decode an ABI encoded revert error. * Throws if the data cannot be decoded as a known RevertError type. * @param bytes The ABI encoded revert error. Either a hex string or a Buffer. * @param coerce Coerce unknown selectors into a `RawRevertError` type. * @return A RevertError object. */ export function decodeBytesAsRevertError(bytes: string | Buffer, coerce: boolean = false): RevertError { return RevertError.decode(bytes, coerce); } /** * Decode a thrown error. * Throws if the data cannot be decoded as a known RevertError type. * @param error Any thrown error. * @param coerce Coerce unknown selectors into a `RawRevertError` type. * @return A RevertError object. */ export function decodeThrownErrorAsRevertError(error: Error, coerce: boolean = false): RevertError { if (error instanceof RevertError) { return error; } return RevertError.decode(getThrownErrorRevertErrorBytes(error), coerce); } /** * 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, true); } catch (err) { if (isGanacheTransactionRevertError(error)) { throw err; } // 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 readonly abi?: RevertErrorAbi; public readonly values: ValueMap = {}; protected readonly _raw?: string; /** * Decode an ABI encoded revert error. * Throws if the data cannot be decoded as a known RevertError type. * @param bytes The ABI encoded revert error. Either a hex string or a Buffer. * @param coerce Whether to coerce unknown selectors into a `RawRevertError` type. * @return A RevertError object. */ public static decode(bytes: string | Buffer | RevertError, coerce: boolean = false): RevertError { if (bytes instanceof RevertError) { return bytes; } const _bytes = bytes instanceof Buffer ? ethUtil.bufferToHex(bytes) : ethUtil.addHexPrefix(bytes); // tslint:disable-next-line: custom-no-magic-numbers const selector = _bytes.slice(2, 10); if (!(selector in RevertError._typeRegistry)) { if (coerce) { return new RawRevertError(bytes); } throw new Error(`Unknown selector: ${selector}`); } const {type, decoder} = RevertError._typeRegistry[selector]; const instance = new type(); try { Object.assign(instance, {values: decoder(_bytes)}); instance.message = instance.toString(); return instance; } catch (err) { throw new Error( `Bytes ${_bytes} cannot be decoded as a revert error of type ${instance.signature}: ${err.message}`, ); } } /** * Register a RevertError type so that it can be decoded by * `RevertError.decode`. * @param revertClass A class that inherits from RevertError. * @param force Allow overwriting existing registrations. */ public static registerType(revertClass: RevertErrorType, force: boolean = false): void { const instance = new revertClass(); if (!force && instance.selector in RevertError._typeRegistry) { throw new Error(`RevertError type with signature "${instance.signature}" is already registered`); } if (_.isNil(instance.abi)) { throw new Error(`Attempting to register a RevertError class with no ABI`); } RevertError._typeRegistry[instance.selector] = { type: revertClass, // @ts-ignore decoder: createDecoder(instance.abi), }; } /** * Create a RevertError instance with optional parameter values. * Parameters that are left undefined will not be tested in equality checks. * @param declaration Function-style declaration of the revert (e.g., Error(string message)) * @param values Optional mapping of parameters to values. * @param raw Optional encoded form of the revert error. If supplied, this * instance will be treated as a `RawRevertError`, meaning it can only * match other `RawRevertError` types with the same encoded payload. */ protected constructor(name: string, declaration?: string, values?: ValueMap, raw?: string) { super(createErrorMessage(name, values)); if (declaration !== undefined) { this.abi = declarationToAbi(declaration); if (values !== undefined) { _.assign(this.values, _.cloneDeep(values)); } } this._raw = raw; // Extending Error is tricky; we need to explicitly set the prototype. Object.setPrototypeOf(this, new.target.prototype); } /** * Get the ABI name for this revert. */ get name(): string { if (!_.isNil(this.abi)) { // @ts-ignore return this.abi.name; } return `<${this.typeName}>`; } /** * Get the class name of this type. */ get typeName(): string { // tslint:disable-next-line: no-string-literal return this.constructor.name; } /** * Get the hex selector for this revert (without leading '0x'). */ get selector(): string { if (!_.isNil(this.abi)) { // @ts-ignore return toSelector(this.abi); } if (this._isRawType) { // tslint:disable-next-line: custom-no-magic-numbers return (this._raw as string).slice(2, 10); } return ''; } /** * Get the signature for this revert: e.g., 'Error(string)'. */ get signature(): string { if (!_.isNil(this.abi)) { // @ts-ignore return toSignature(this.abi); } return ''; } /** * Get the ABI arguments for this revert. */ get arguments(): DataItem[] { if (!_.isNil(this.abi)) { // @ts-ignore return this.abi.arguments || []; } return []; } get [Symbol.toStringTag](): string { return this.toString(); } /** * 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 RevertError instance, hex-encoded bytes, or a Buffer of the ABI encoded revert. * @return True if both instances match. */ public equals(other: RevertError | Buffer | string): boolean { let _other = other; if (_other instanceof Buffer) { _other = ethUtil.bufferToHex(_other); } if (typeof _other === 'string') { _other = RevertError.decode(_other); } if (!(_other instanceof RevertError)) { return false; } // If either is of the `AnyRevertError` type, always succeed. if (this._isAnyType || _other._isAnyType) { return true; } // If either are raw types, they must match their raw data. if (this._isRawType || _other._isRawType) { return this._raw === _other._raw; } // Must be of same type. if (this.constructor !== _other.constructor) { return false; } // Must share the same parameter values if defined in both instances. 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); // @ts-ignore if (!checkArgEquality(type, a, b)) { return false; } } } return true; } public encode(): string { if (this._raw !== undefined) { return this._raw; } if (!this._hasAllArgumentValues) { throw new Error(`Instance of ${this.typeName} does not have all its parameter values set.`); } const encoder = createEncoder(this.abi as RevertErrorAbi); return encoder(this.values); } public toString(): string { if (this._isRawType) { return `${this.constructor.name}(${this._raw})`; } const values = _.omitBy(this.values, (v: any) => _.isNil(v)); // tslint:disable-next-line: forin for (const k in values) { const {type: argType} = this._getArgumentByName(k); if (argType === 'bytes') { // Try to decode nested revert errors. try { values[k] = RevertError.decode(values[k] as any); } catch (err) { } // tslint:disable-line:no-empty } } const inner = _.isEmpty(values) ? '' : inspect(values); return `${this.constructor.name}(${inner})`; } private _getArgumentByName(name: string): DataItem { const arg = _.find(this.arguments, (a: DataItem) => a.name === name); if (_.isNil(arg)) { throw new Error(`RevertError ${this.signature} has no argument named ${name}`); } return arg; } private get _isAnyType(): boolean { return _.isNil(this.abi) && _.isNil(this._raw); } private get _isRawType(): boolean { return !_.isNil(this._raw); } private get _hasAllArgumentValues(): boolean { // @ts-ignore if (_.isNil(this.abi) || _.isNil(this.abi.arguments)) { return false; } // @ts-ignore for (const arg of this.abi.arguments) { if (_.isNil(this.values[arg.name])) { return false; } } return true; } } const PARITY_TRANSACTION_REVERT_ERROR_MESSAGE = /^VM execution 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[]; } interface ParityTransactionRevertError extends Error { code: number; data: string; message: string; } /** * Try to extract the ecnoded revert error bytes from a thrown `Error`. */ export function getThrownErrorRevertErrorBytes( error: Error | GanacheTransactionRevertError | ParityTransactionRevertError, ): 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 if (isParityTransactionRevertError(error)) { // Parity returns { data: 'Reverted 0xa6bcde47...', ... } const {data} = error; const hexDataIndex = data.indexOf('0x'); if (hexDataIndex !== -1) { return data.slice(hexDataIndex); } } 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 Error "${error.message}" as a RevertError`); } function isParityTransactionRevertError( error: Error | ParityTransactionRevertError, ): error is ParityTransactionRevertError { if (PARITY_TRANSACTION_REVERT_ERROR_MESSAGE.test(error.message) && 'code' in error && 'data' in error) { return true; } return false; } 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('StringRevertError', 'Error(string message)', {message}); } } /** * Special RevertError type that matches with any other RevertError instance. */ export class AnyRevertError extends RevertError { constructor() { super('AnyRevertError'); } } /** * Special RevertError type that is not decoded. */ export class RawRevertError extends RevertError { constructor(encoded: string | Buffer) { super( 'RawRevertError', undefined, undefined, typeof encoded === 'string' ? encoded : ethUtil.bufferToHex(encoded), ); } } /** * 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)'). * @return A RevertErrorAbi object. */ function declarationToAbi(declaration: string): RevertErrorAbi { 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) => { // Match a function parameter in the format 'TYPE ID', where 'TYPE' may be // an array type. m = /^\s*(([_a-z][a-z0-9_]*)(\[\d*\])*)\s+([_a-z][a-z0-9_]*)\s*$/i.exec(a); if (!m) { throw new Error(`Invalid Revert Error signature: "${declaration}"`); } // tslint:disable: custom-no-magic-numbers return { name: m[4], type: m[1], }; // tslint:enable: custom-no-magic-numbers }); const r: RevertErrorAbi = { type: 'error', name, arguments: _.isEmpty(argData) ? [] : argData, }; return r; } function checkArgEquality(type: string, lhs: ArgTypes, rhs: ArgTypes): boolean { // Try to compare as decoded revert errors first. try { return RevertError.decode(lhs as any).equals(RevertError.decode(rhs as any)); } catch (err) { // no-op } if (type === 'address') { return normalizeAddress(lhs as string) === normalizeAddress(rhs as string); } else if (type === 'bytes' || /^bytes(\d+)$/.test(type)) { return normalizeBytes(lhs as string) === normalizeBytes(rhs as string); } else if (type === 'string') { return lhs === rhs; } else if (/\[\d*\]$/.test(type)) { // An array type. // tslint:disable: custom-no-magic-numbers // Arguments must be arrays and have the same dimensions. if ((lhs as any[]).length !== (rhs as any[]).length) { return false; } const m = /^(.+)\[(\d*)\]$/.exec(type) as string[]; const baseType = m[1]; const isFixedLength = m[2].length !== 0; if (isFixedLength) { const length = parseInt(m[2], 10); // Fixed-size arrays have a fixed dimension. if ((lhs as any[]).length !== length) { return false; } } // Recurse into sub-elements. for (const [slhs, srhs] of _.zip(lhs as any[], rhs as any[])) { if (!checkArgEquality(baseType, slhs, srhs)) { return false; } } return true; // tslint:enable: no-magic-numbers } // tslint:disable-next-line return new B.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 createEncoder(abi: RevertErrorAbi): (values: ObjectMap<any>) => string { const encoder = AbiEncoder.createMethod(abi.name, abi.arguments || []); return (values: ObjectMap<any>): string => { const valuesArray = _.map(abi.arguments, (arg: DataItem) => values[arg.name]); return encoder.encode(valuesArray); }; } function createDecoder(abi: RevertErrorAbi): (hex: string) => ValueMap { const encoder = AbiEncoder.createMethod(abi.name, abi.arguments || []); return (hex: string): ValueMap => { return encoder.decode(hex) as ValueMap; }; } function toSignature(abi: RevertErrorAbi): string { const argTypes = _.map(abi.arguments, (a: DataItem) => a.type); const args = argTypes.join(','); return `${abi.name}(${args})`; } function toSelector(abi: RevertErrorAbi): string { return ( ethUtil .keccak256(Buffer.from(toSignature(abi))) // tslint:disable-next-line: custom-no-magic-numbers .slice(0, 4) .toString('hex') ); } // Register StringRevertError RevertError.registerType(StringRevertError); // tslint:disable-next-line max-file-line-count